Live data from Hacker News

Tests aren’t enough: Case study after adding type hints to urllib3

sethmlarson.dev

191–200 of 205 posts

Re: Tests aren’t enough: Case study after adding type hints to urllib3

#191
post #151

Earlier quoted context omitted.

This is true if you're focused on the compiler but if you include the adjacent question of what is reported to you in your editor while working it's a bit blurrier. If I write some Rust code and pass a string into something which expects an integer, I get a hard error preventing compilation. If I do the same thing in Python, however, and there's a prominent error displayed in my editor before I even run the code, how…

Both your examples are the same static typing. One just has better IDE integration than the other.

In the first case, there would be a hard compiler error — rustc would refuse to compile the code until I fixed it.

In the second case, Python would allow the code to run but would potentially produce a runtime TypeError when it reached that point depending on exactly the code does. It might also run fine (e.g. I'm just passing that variable to json.dump()) or produce unexpected output (e.g. I'm passing that code to print() and that worked for int, and str, but then someone called it with None and I didn't want "None" in the output).

The point was that while those differ in how they're implemented, the experience can be fairly similar when you're in the middle of the code-test cycle. My example wasn't the most complicated dynamic typing scenario but it's an example of why this works pretty well: most Python code isn't highly dynamic or dynamic everywhere — typically there are a few places which might be challenging for analysis but there's also a LOT of code which only ever works with a single input type. If your IDE provides feedback on all of that code, you're going to avoid a fair number of other bugs and free up time for the hard parts.

Re: Tests aren’t enough: Case study after adding type hints to urllib3

#192

Earlier quoted context omitted.

I personally love it, and wish every library worked this way. My argument is why go out of my way to make it not work, when it would be easy to make it work. This is because I think of modules/packages as user facing programs that are easy to tie together, instead of simple building blocks. What I really wish existed was a built in way to cast and validate, or normalize and validate. I never care if something is a st…

> why go out of my way to make it not work, when it would be easy to make it work The problem the DWIM approach to APIs is that when you go out of your way to "do something reasonable" with absolutely any kind of argument type, leaving the caller's intent implicit, you will sometimes run into combinations that "work" in unexpected—and often unwanted—ways. For example, say you have a function which returns either a Pe…

For example, say you have a function which returns either a Person object or, in very rare cases, an error string. Moreover, you fail to check for the error string, and pass the result into another function which expects a Person object but will also take a name and look up the corresponding Person object in a table. Now if the first function fails you're left trying to look up an error string as a name, with no obvious signs (such as a type mismatch error) to show that anything is amiss.

Well I only ever return one type from a function, I'm not a total madman. Sometimes I'll do one type or a None, if I'm trying to replicate the functionality of dict.get(). Any error string would be within an Exception, so that wouldn't be an issue, but even in your example it would show a stack trace to the function looking up the user, and would be much more valuable to troubleshoot than a type mismatch.

One option compatible with both statically- and dynamically-typed languages is to provide two functions, one requiring a Person object and another taking a name string. This is still perfectly ergonomic for the user and mitigates most of the potential for confusion.

In practice that is usually what I end up doing, but with a 3rd function that takes either and returns a Person object. In this particular case I would probably make the function be a method on the Person object, and have a class method to look up the Person.

Here is the scenario that annoyed me enough to turn me off static typing. I had a class that stored the IP address of a network device as an ipaddr.IPAddress object (now ipaddress in the standard library) and there were various subclasses for specific device types. One of the device types needed an SDK, and the init for the SDK class looked something like this

  def __init__(self, host, port=1234, scheme='https'):

      if not isinstance(ip, str):
         raise TypeError('invalid host')

      self.url = f"{scheme}://{host}:{port}"

If they didn't check the type it would have worked fine. Just like every other library we were using to connect to devices.

So after a bit of frustration we changed our base class

  def original__init__(self, ip_address):
      self.ip_address = ipaddr.IPAddress(ip_address)
  def new__init__(self, ip_address):
      ipaddr.IPAddress(ip_address) # just to validate
      self.ip_address = ip_address 
and all was well with the world, but there was a dumb mistake waiting for us. A year or two later, after upgrading to 2.7 we started passing around unicode objects instead of strings to get ready for 3.x, as was the style at the time. Again that SDK broke, and only that SDK, because it insisted on checking the type. Sure it was our mistake this time for not having the original fix to be just casting it to str right before passing it to the SDK, but it was annoying and should have been unnecessary.

I understand that type hints are much better in this regard because it would only show an error in your tooling. But that brings me to another point.

I write my packages/classes/modules to mostly be used in a web app, or as scripts that run on a schedule. However, I also need to be able to write one-offs very quickly. When that happens my code that was previously a library for different applications, now becomes an application itself. Using the REPL, a jupyter notebook, or bpython, I will need to quickly get something done. In these scenarios I don't want to waste time remembering how to normalize the data being given to me. Especially If the code that provides such niceties is tucked away at a higher level for end users of the web app.

Like I said, I tend to just make a lookup function, and then have everything else be methods on the object. But that doesn't really help when it's parameters to a function. I really don't know what would make it better. Perhaps some kind of mix between function overloading and interfaces from other languages, and the magic *_validate() methods that Django uses. Maybe instead of type hints for return values we need value hints, that give an idea of what actual objects might look like. Then tooling could take into account if it would still work after validation and normalization. Of course it could be that there is no elegant and reliable way to do what I really want, but I can dream.

Re: Tests aren’t enough: Case study after adding type hints to urllib3

#193
post #155

Earlier quoted context omitted.

> Take game systems, as an example; far far more effort will be spent in the art and general asset management than is true for many business software setups. Which is why many of the business best practices haven't necessarily moved over to games. You're right that game development involves a lot of asset stuff that other business software doesn't have to worry about as much. (And, conversely, a lot of business softw…

I can't say with any authority on why practices are different between the different environments. So, to that end, I should have offered it as /a/ reason, as I don't think it is the sole one. I can't shake that it contributes, though. I mainly meant it is a counter to the implicit "devs at office job are too lazy to learn different ways." For other examples, I would dip into major logistical simulations/optimizations…

You're welcome! :)

Re: Tests aren’t enough: Case study after adding type hints to urllib3

#194

Earlier quoted context omitted.

> why go out of my way to make it not work, when it would be easy to make it work The problem the DWIM approach to APIs is that when you go out of your way to "do something reasonable" with absolutely any kind of argument type, leaving the caller's intent implicit, you will sometimes run into combinations that "work" in unexpected—and often unwanted—ways. For example, say you have a function which returns either a Pe…

For example, say you have a function which returns either a Person object or, in very rare cases, an error string. Moreover, you fail to check for the error string, and pass the result into another function which expects a Person object but will also take a name and look up the corresponding Person object in a table. Now if the first function fails you're left trying to look up an error string as a name, with no obvi…

> Well I only ever return one type from a function, I'm not a total madman.

I'm sure your APIs are sane (at least to you). It's all the other developers you have to watch out for.

> … even in your example it would show a stack trace to the function looking up the user, and would be much more valuable to troubleshoot than a type mismatch.

A type mismatch would be caught earlier (even in a dynamic language) and the runtime exception should report the specific objects involved, so you still get the string which caused the problem.

> Here is the scenario that annoyed me enough to turn me off static typing.

To begin with, this example has nothing to do with static typing. It involves a runtime time check. In this case I would agree that the type check is too strict. Some languages have an interface or protocol for "string-like" objects (e.g. the to_str method in Ruby), and it would be better to use that rather than checking specifically for an instance of str. Objects which shouldn't be treated as strings just don't implement the protocol. Python has the __str__ magic method, but unfortunately it's not very useful in this regard since all objects implement it, even ones that are nothing like strings. It's more like Ruby's to_s method, used for formatting and debugging rather than as an indication that you have an actual string. The best recommendation I've seen for checking for "string-like" objects in Python is something like `str(x) == x`, though the extra comparison adds some overhead.

Of course that doesn't really help you since you were trying to pass an arbitrary non-string-like object (IPAddress) to a function expecting a string; the looser `str(x) == x` check would also have failed. The call might have "just worked" without the condition, or it might have failed spectacularly. In assuming that it would work without the type check you're depending on the implementation using string interpolation rather than, say, concatenating the strings with the + operator, which requires actual strings and not IPAddress objects since the + operator doesn't do implicit conversion like f-strings would. Static typing would have helped to limit these dependencies on unstable implementation details, letting you know that you need to fix the issue at the call site by passing `str(self.ip_address)` for the host parameter.

Re: Tests aren’t enough: Case study after adding type hints to urllib3

#195
post #184

Earlier quoted context omitted.

It's the return type that isn't decidable. You can't statically check, in the arbitrary case, whether the function returns an int or a string.

No, but that isn't required for a static type system. Most languages would just unify to the top type.

Could you elaborate on that? It sounds like I may have an overly simplified understanding of the topic here—wouldn't be the first time.

Re: Tests aren’t enough: Case study after adding type hints to urllib3

#196

Earlier quoted context omitted.

> Vararg functions also have limited use. This is only because people are using statically typed language that place arbitrary restrictions on such functions and make them harder to use. In dynamically typed languages, vararg functions are widely used and enable patterns that are pretty nice.

Perhaps. But then I want to know what those patterns are, what are their actual benefits compared to not using them, and most of all I want to know if such benefits outweigh the significant costs that comes with the lack of static analysis¹. [1] The need to test much more, the need for a better, more accurate documentation, the higher cost of refactoring, even the higher prototyping times (I prototype faster with a R…

> I prototype faster with a REPL that has static typing, because I don't to debug type errors

On the other hand, because Common Lisp has resumable exceptions and on-the-fly redefinition of just about everything, I prototype significantly faster in CL because I can just let the debugger stay open until I fix the issue and then hit “continue”.

I no longer believe the “static faster for development than dynamic” thesis because I think a lot depends on how the programmer thinks about programming and which tools are available.

Re: Tests aren’t enough: Case study after adding type hints to urllib3

#197

I love static typing/type hints if for only 1 thing - code maintenance. Even code I wrote six months ago. Not having to dig through 6 functions deep to try to figure out whether "person" is a string, or an object, and if it's an object what attributes it has on it etc. is huge. And not to mention that some clever people decide - hey, if you pass a string I'll look up the person object - so you can pass an object or a…

The main argument for dynamic typing is speed in prototyping but I find that's opposite for me. I'm much more comfortable rapid prototyping and ripping stuff apart when I have a strongly static typed environment telling me what I just broke. Doing radical refactoring often involves just making those changes and then fixing all the IDE or compiler errors until it runs again.

Agree. I usually think types first and quickly sketch the whole application without writing any code. So,when I start writing code it just works end-to-end.

Re: Tests aren’t enough: Case study after adding type hints to urllib3

#198
post #17

Earlier quoted context omitted.

I’ve done everything from Haskell to Java and I still strongly prefer Clojure and Common Lisp-style dynamic types.

I have to agree, I've done over 5 years of C# and then went to ruby and never looked back. Static type checking raises the floor on incompetence, but also lowers the ceiling on excellence. I have to admit I don't have experience with the extremes which would be Haskell and Clojure. The amount of cruft I had to type in C# just to get shit done... It's all implicit in ruby thank god for that. I never EVER have to check…

> lowers the ceiling on excellence

There is zero real world evidence for that statement. The smartest developers I have ever worked with love types. The not-so-smart ones couldn’t figure out how to use types well and their code was a buggy mess. Not evidence of anything of course but certainly a sample point.

Re: Tests aren’t enough: Case study after adding type hints to urllib3

#199
post #46
post #32

Earlier quoted context omitted.

How in the world does type checking lower the ceiling on excellence?

I"m guessing by rejecting perfectly valid and correct programs that are unable to be type checked. There is a large space of "false negative" programs that a type checker will reject, but that could be perfectly correct. E.g. compare Python-esque duck typing with nominal typing.

[deleted]

Re: Tests aren’t enough: Case study after adding type hints to urllib3

#200

Earlier quoted context omitted.

Perhaps. But then I want to know what those patterns are, what are their actual benefits compared to not using them, and most of all I want to know if such benefits outweigh the significant costs that comes with the lack of static analysis¹. [1] The need to test much more, the need for a better, more accurate documentation, the higher cost of refactoring, even the higher prototyping times (I prototype faster with a R…

> I prototype faster with a REPL that has static typing, because I don't to debug type errors On the other hand, because Common Lisp has resumable exceptions and on-the-fly redefinition of just about everything, I prototype significantly faster in CL because I can just let the debugger stay open until I fix the issue and then hit “continue”. I no longer believe the “static faster for development than dynamic” thesis…

Yeah, I've heard about image based programming, that enables editing your program as you run it. It scares the shit out of me.

See, if I have an unexpected error, that's because I fucked up my program. And because of that, my runtime state is likely screwed as well. So not only do I have to correct my error, I have to correct its consequences before I restart the program. I can't just resume its execution and hope for the best, I need to know that whatever state I keep is not rotten.

On the other hand, that way of doing things is not exclusive to dynamic languages. There's thing things called "dynamically loaded libraries", that you can use even in C. Game programmers routinely recompile & reload specific dlls just so they can correct their mistakes without restarting the whole game. And those who have written in-game editors have a very powerful stop-debug-restart cycle. On top of a statically typed language.

Post reply on HN