8 ms·
Static Duck Typing in Python with Protocols
- zbentley 5y agoBasic as it is, it is worth reminding folks reading this and similar articles that Python, even with type annotations, does not have a compile pass and that IDEs like PyCharm tend to fail open when they can't figure out how to type check something. I see a lot of folks coming to Python from TypeScript and assuming that there's any enforcement of type annotations a la tsc. There's not, not unless you run a tool like MyPy on your code and configure it to be strict enough to provide the type assurance you need (what level of strictness is appropriate depends on the project and the programmers). I also see folks getting lulled into a false sense of security by their IDE. PyCharm is great, and it highlights many typing errors in code as you write them. But it gives up very easily in the presence of any indirection at all (many basic decorators which don't change function signatures, for example, are enough to cause it to stop trying to type check decorated functions at all). And when it gives up, there's no indication of that; giving up looks the same as error-free code. That's not an indictment of any tools. Python is the way it is for good reason, and it's type annotation system is good and getting better all the time (e.g. Protocols as discussed in this article). PyCharm is also super helpful and given how it performs it's type checking it's borderline miraculous that it works as well as it does in the first place. Rather, this is a reminder, especially for new python programmers: don't assume that type annotations give you anything (except documentation, which it's on you to keep accurate) for free.
- tyingq 5y agoI'm not an expert in either, but the difference doesn't seem dramatic to me. You can put type annotations in JS also and just use typescript in a way that's similar to MyPy. Adding types to either JS or Python seems "not free" to me. You pull in extra tools and syntax for either.
- zbentley 5y agoIn TypeScript, there's no* way to run your code without invoking the type checker, as it has to transpile TS to JS. While you can suppress/reduce the strictness of the checker, you can't avoid it entirely, and opting out takes work. Python is the opposite; using type annotations for any sort of verification is opt-in exclusively, and not part of the language/runtime itself. *Yes, there are some direct TS runtimes/compilers, but they aren't how the vast majority of TS code runs.
- mynameisash 5y agoI just got bit tonight by Python's duck typing. I was trying to use PIL to draw rectangles for a super simple script to visualize results from another component. I found an example and tweaked accordingly: w, h = 220, 190 shape = [(40, 40), (w - 10, h - 10)] # ... img.rectangle(shape) When my results came out totally wrong, I spent 45 minutes trying to figure out if my code that built the shapes was off or if my drawing was off. What I had assumed were rectangle dimensions in the form of `(width, height)` were actually bounding box coordinates `(x2, y2)`. Both of those are tuples of ints, but semantically, they're very different. I'm growing increasingly frustrated with Python, and in my frustration, I feel like this nonsense could be wholly avoided by stronger typing. Were I to use Rust or C#, two languages with which I have much more experience, I have to imagine the library would prevent this loss of productivity. Simply define a Point struct and it's perfectly clear what is being passed in. Edit: I shouldn't have said duck typing but rather the lack of strong/compile checks. Apologies for my sloppy thinking.
- pgorczak 5y agoYou don’t need strong typing but good naming to avoid these issues. While a type system kind of requires everything to be named, it really doesn’t matter to the type checker whether these names are semantically correct or have any semantic meaning at all.
- framecowbird 5y agoThere's nothing stopping you doing that in Python. If it's important to you to define a point struct, do that, add a type annotation, and add mypy to your compile. When i used to work with C++ i would spend hours debugging compile issues that were just the wrong type (templates are especially bad!) that really had no impact on the code, it was just a waste of my time. What I like about python is that the static typing is there if you want it, but you don't have to.
- still_grokking 5y agoTo my knowledge Python doesn't have any sound type system. Mypy doesn't provide that. So, no you can't have (currently) proper static typing in Python. It's more like in TypeScript where it's a best effort thing. (At least you get notified when the typer gives up. That's much better than nothing of course!) C++ templates aren't a good example either. Templates are a Turning complete language so they can't provide guaranties in all cases and aren't as such type safe at all. Better examples for type systems and their usefulness would be languages form the ML family (also including Haskell and similar in this case).
- JackMcMack 5y agoPyCharm has some longstanding issues with type checking aliased and union types. My fix coming in 2021.3.1 [0] [0] https://github.com/JetBrains/intellij-community/pull/1655 https://github.com/JetBrains/intellij-community/pull/1655
- nitred 5y agoI face these issues on the daily. Pycharm's type checking is sufficient for my use case but the TypeVar and Union type checks are a bother and I always pay close attention to it. Really appreciate the fix(es)!
- ossopite 5y agopeople talk a lot about MyPy but I must recommend Pyright as a better alternative. It's much more robust. In my experience the main problem with MyPy is that when it's unsure about what's going on (especially due to imports it doesn't understand, sometimes because of misconfiguration) it just gives up on type checking with absolutely no warning. Pyright does not have this failure mode and handles Unknown types properly (with configurable strictness)
- ramraj07 5y agoHave you compared pytype ? Want to know if pyright is wort switching.
- dragonwriter 5y ago> Basic as it is, it is worth reminding folks reading this and similar articles that Python, even with type annotations, does not have a compile pass This is literally false, Python does have a compilation phase, and with its default settings it saves the compiled results for reuse. You probably mean a static analysis phase, and that's...up to your workflow. If you are using mypy, pyright/pylance, or a other typechecker, it does have a static type analysis phase, that's what those are. And if you are getting typw feedback in an IDE, that's probably what you are doing. > PyCharm is great, and it highlights many typing errors in code as you write them. But it gives up very easily in the presence of any indirection at all (many basic decorators which don't change function signatures, for example, are enough to cause it to stop trying to type check decorated functions at all) Most of Python's major typecheckers (not sure about PyCharm’s custom one, definitely Pyright/Pylance and mypy) have configurable levels of strictness. They can definitely be configured not to give up easily. But a decorator is a function, and if it is untyped rather than expressing how it transforms the wrapped function signature—even if it does not, in fact, transform the function signature—then it will probably have an Any type (none of typecheckers are great at inference) and either defeat or fail typechecking, depending on strictness settings. > Rather, this is a reminder, especially for new python programmers: don't assume that type annotations give you anything (except documentation, which it's on you to keep accurate) for free. Annotations alone don't give you anything (and aren't free, as you have to crrate them), but static analysis definitely gives you something.
- zbentley 5y agoYou're right, my phrasing was inaccurate. The intent was: Python does not perform type checking based on annotations (e.g. Protocols as discussed in the article) when it starts. Unfortunately PyCharm's analyzer is pretty fundamentally oriented towards failing open, rather than closed. For decorators, PyCharm often infers whether or not a decorator changes a signature. However, it does this inconsistently (and it may be backed by a specific list of supported decorators by name/module, I'm not sure). When it can't tell what a decorator does, it doesn't try to validate calls to decorated functions, which is kind of a bummer. What I'd really like is a way to tell the IDE that unrecognized/unparseable decorators should be assumed not to emit functions with a different signature; that would be wrong sometimes, but useful in what is for me the most common case.
- yeswecatan 5y agoI'm very curious to hear where people stand on Protocols vs ABCs. While I definitely see the appeal of Protocols, I _think_ I prefer ABCs. Realistically it probably depends on the situation.
- DandyDev 5y agoArticle author here. To me the big advantage of Protocols as opposed to ABCs, is that you don't have to have your classes implement a special base class and can instead focus on the _consuming_ code. Using a Protocol for a function parameter works even for passing in objects of classes you don't control.
- pgorczak 5y agoI think it boils down to thinking in composition (x has a y) vs inheritance (x is a y). I’m happy about protocols because most of the times, coming up with some pattern or order of inheritance using ABCs really feels beside the point of what I’m trying to achieve (stating that an object _has_ a specific set of attributes).
- gjulianm 5y agoIn my case I mostly use ABCs, not because I think they're universally better but because my use case fits them: dealing mostly with my own types, base classes provide some common functionality and the number of types is limited. Protocols are great for the opposite cases: you use types that are not necessarily under your control, no shared functionality and a wider amount of types.
- formerly_proven 5y agoABCs lend themselves well to create inheritance-heavy code with hierarchies, which tends to become rather messy quick.
- ragebol 5y agoArticle mentions a type `List[Union[TemperatureMeasurement, HumidityMeasurement]]` to average over, so a list of potentially mixed temp and humidity. Which I guess you could average but has no physical meaning (though not the point of the article). I think what is meant is Union[List[TemperatureMeasurement], List[HumidityMeasurement]], , so a list of either just TemperatureMeasurements or a list of only HumidityMeasurements.
- DandyDev 5y agoArticle author here. You're absolutely right of course! Thanks for pointing this out. I immediately fixed it.
- mjburgess 5y agoYou also say, > you care only about the behaviour ... not the explicit type of that input "duck typing" is a run-time equivalent of structural typing. It would be a little more accurate to say the value's properties (ie., the object behaviour) *is* the type. A "protocol" isnt a means of "ignoring the real type, ie., the name we give a type". It is a means of expressing a structural type. Nominal typing, ie., a type "being some name" isn't fundamental. Indeed, my biggest complaint with python's static typing efforts are how hamfisted nominal typing has been shoved on top of python, as-if it were just "failing to be java".
- radarsat1 5y ago> Indeed, my biggest complaint with python's static typing efforts are how hamfisted nominal typing has been shoved on top of python, as-if it were just "failing to be java". This is the best way I've seen my feelings on this summarized. Thanks! I really find that efforts to bring nominal typing to Python really just restrict it, and it becomes not the same language anymore. If I want that kind of typing, I prefer to use a language that does it properly. Otherwise, I want a type system that works for how Python is designed. I'm really finding these days that I have to restrict the range of types my functions are designed to work on just to satisfy a bunch of linters. It's very annoying. And imho goes against the grain of why I chose Python in the first place.
- abdulhaq 5y agoI've used Enthought's Traits very extensively for a form of static type checking in Python, and I can strongly recommend it.
- ltbarcly3 5y agoType checking in Python is a complete waste of time and only leads to a false sense of improvement. People use type annotations for the same reason they wear a shirt in the pool, it feels weird to not see those reassuring type names everywhere once you are used to them from some other language. Take out all the non enforced type annotations and spend the time and effort you put into them into writing tests and docstrings instead.
- radarsat1 5y agoOne thing that drives me nuts is that enthusiasts of python type hints claim that it means you will no longer give a function the wrong type of value and thus eliminate a class of errors. This should imply that you no longer need to test for those types of problems. Since writing tests is not free, and often not even trivial, this would be a huge benefit of type hints. But, because type hints are just window dressing in Python, you still have to write those tests. So now when you make a change, you not only have to update your tests but also update your type hints everywhere. As far as I can tell, type hints just double the work for little to no benefit. If they actually removed a need for unit testing, that would be a different story, but then it wouldn't be Python anymore. If I want to use types to enforce things, I'd prefer to use a language designed for that. But I choose Python because it lets me get things done quickly. Despite helping you out in some cases, overall maintaining type hints are a drag on that feeling of getting things done quickly. The last thing I want to do when writing Python is spend an afternoon composing a complicated TypeVar arrangement to express the full range of inputs my function can handle. What this article talks about (Protocol) seems to be at least an improvement in that regard, as you can worry about what your function does instead of what your types do.
- dragonwriter 5y ago> But, because type hints are just window dressing in Python, Type annotations are not window dressing in a Python workflow using typechecking tools over the final code base, unless you explicitly tell the typechecker to ignore a detected error. It's true that a type annotated library can't necessarily rely on downstream clients to have type discipline, and some library packages may not be fully annotated, which may create gaps to address. > If I want to use types to enforce things, I'd prefer to use a language designed for that. Python's type annotations were designed largely to make Python compatible with a separate existing language designed for that purpose; there's a reason mypy’s domain is mypy-lang.org > What this article talks about (Protocol) seems to be at least an improvement in that regard, as you can worry about what your function does instead of what your types do. Protocols are types; specifically, they are structural types.
- radarsat1 5y agoQuestion. The idea here is to introduce a type declaring what the requirements are of the input arguments, right? The article's example: class MeasurementLike(Protocol): timestamp: datetime declares that the object needs a field called timestamp. This is a contract that ensures the following line of code can work: window_upper_bound = measurements[0].timestamp + window_size The benefit being promoted by the article here, which I agree with, is that you only need to declare what the actual code requires and not name the full type of the expected input. So, for me this raises the following: why can't this be done automatically? I can look at the code directly and see that measurements is a list with items that require a field called timestamp. Why is it necessary to write a bunch more code to tell this to a linter, when it's directly inferable from the code itself? I can see directly that measurements must contain objects with timestamp, and that timestamp must be some object that can be added to whatever window_size is. If it's entirely possible to get this information from the code, I don't really understand why it's necessary to burden the programmer with type hinting at all. It seems like the job for a type inference engine. If I have to repeat a bunch of information that the compiler/(interpreter/linter..) should be able to figure out, as far as I can tell all it adds is (1) more work to do and (2) more opportunities to make mistakes. The idea of writing it all out explicitly is often celebrated as some kind of assurance of correctness; but you often figure out the types after, or in tandem, with writing the code. So it effectively is just like a "check", like those systems that make you type your password twice to make sure you didn't mistype it. Sure, it serves a purpose in the moment, but is it necessary to add maintenance burden, forever, to your code base for something that is totally redundant?
- joconde 5y agoDeclaring the types manually ensures that you don’t change the interface by accident. If I start using an attribute of an argument that wasn’t required before, I’ll get an error from the linter that will prevent me from breaking user code.