We're sorry but this page doesn't work properly without JavaScript enabled. Please enable it to continue.
Feedback

Protocols in Python: Why You Need Them

00:00

Formal Metadata

Title
Protocols in Python: Why You Need Them
Title of Series
Number of Parts
112
Author
Contributors
License
CC Attribution - NonCommercial - ShareAlike 4.0 International:
You are free to use, adapt and copy, distribute and transmit the work or content in adapted or unchanged form for any legal and non-commercial purpose as long as the work is attributed to the author in the manner specified by the author or licensor and the work or content is shared also in adapted form only under the conditions of this
Identifiers
Publisher
Release Date
Language

Content Metadata

Subject Area
Genre
Abstract
Protocols have been around since Python 3.8. So what are they, and how can they help you write better code? And how are they different from Abstract Base Classes? In this talk I will introduce you to both concepts (ABCs and Protocols), and show you by example how they can make your life easier, and your code cleaner.
29
GoogolGamma functionOrder (biology)Communications protocolType theoryFluid staticsParticle systemLecture/Conference
Magneto-optical driveGamma functionData typeFunctional (mathematics)Parameter (computer programming)IntegerDampingType theoryCASE <Informatik>Attribute grammarCodeError messageCommunications protocolFluid staticsRight angleSocial classDynamical systemPoint (geometry)Computer configurationNumberDeclarative programmingObject (grammar)Run time (program lifecycle phase)ResultantFormal languageOrder (biology)Function (mathematics)Operator (mathematics)Instance (computer science)Category of beingComputer programmingVirtual machineComplex (psychology)Multiplication signCoefficient of determinationWordWebsiteDigital photographyNetwork topologyMereologyProgramming languageCore dumpDescriptive statisticsThomas BayesTrailParticle systemBucklingExistential quantificationPlastikkarteArithmetic meanComputer animation
MassParameter (computer programming)Maxima and minimaGamma functionParameter (computer programming)Functional (mathematics)AbstractionCoefficient of determinationInheritance (object-oriented programming)MaizeInstance (computer science)TypprüfungCodeType theoryLibrary (computing)Social classInterface (computing)Presentation of a groupComputer configurationFluid staticsAdaptive behaviorPerfect groupObject (grammar)Run time (program lifecycle phase)CASE <Informatik>Multiplication signGodProgram slicingRight angleGradientDeclarative programmingImplementationGroup actionLie groupShape (magazine)Plotter
Type theoryAbstractionCommunications protocolSocial classInstance (computer science)CASE <Informatik>Multiplication signQuicksortFunctional (mathematics)IterationSound effectAttribute grammarFluid staticsDynamical systemFormal languageRun time (program lifecycle phase)Context awarenessLibrary (computing)Parameter (computer programming)MultiplicationElectronic signatureData structureDifferent (Kate Ryan album)ExistenceObject (grammar)Point (geometry)ImplementationPhysical lawOffice suitePrototypeParticle systemMarginal distributionAnalogyNumberVirtualizationRow (database)WebsiteMereologyComputer animation
Type theoryDemosceneBit rateAttribute grammarObject (grammar)CASE <Informatik>Particle systemExistenceMultiplication signBitSingle-precision floating-point formatElectronic signatureSocial classCommunications protocolLecture/Conference
Run time (program lifecycle phase)Type theoryObject (grammar)Multiplication signSocial classWebsiteQuicksortLecture/Conference
Functional (mathematics)ExistenceDisk read-and-write headQuicksortAttribute grammarCommunications protocolRun time (program lifecycle phase)Lecture/Conference
Communications protocolImplementationError messageFunctional (mathematics)Run time (program lifecycle phase)Attribute grammarSocial classLecture/Conference
Communications protocolCategory of beingMultiplication signProcess (computing)Lecture/Conference
Lecture/Conference
Transcript: English(auto-generated)
I'm a data charmer at Go Data Driven, don't ask me what that actually means, and I want to talk to you about protocols and why you actually need them, or in fact, mostly what
are protocols. Now in order to fully appreciate the usefulness of protocols to see why you actually need them, I think it's useful to go all the way from, well, what is dynamic versus static typing, type hints, then ABCs, and finally protocols.
So if you're familiar with one or more of the first concepts, bear with me, all the way in the end I'll talk about protocols. So let's first talk about typing in Python. So Python is a dynamically typed language.
You might know that. So what does that mean? So let's have a look at what dynamic typing versus static typing is. So here on the left side you see some Python code, so a function, my function, and this takes three arguments, A, B and C, and then when you call it, it returns some kind of
so it does an operation on those arguments and then returns the results. And so the return type of this function, it depends on the arguments that you put in. So if you put in 5, 3 and 2, you will get an integer as output.
On the other hand, if you put in 5.1, 3 and 2, you will get a floating point number. These types, they are checked at run time. And as you can see, type declarations are not required. I haven't defined any types in the definition of the function, and the only way we can
or the only way I have played around with the types is by just the values of the arguments I put in there. On the other hand, so on the right you see an example of a statically typed language. So in this case we have to specify the return type of the function, in this case it is an
integer, and then the function name with three arguments, again, A, B and C, and those three arguments are all integers. It explicitly says that. So now when you in such a language, when you try to call this function with three integers, you will get an integer back, and when you do this with a float as one of the
arguments, you will get an error. And these types are not checked at run time, but they are checked at compile time. This does mean that type declarations are required.
As you can see, both the return types as well as the argument types are specified there, right in the code. So there are pros and cons of both kinds of languages with dynamic typing, you don't have to really worry so much, as long as the arguments that you put in there work
with the operations that you do in the function, everything is fine, but if something is not fine, you might only find out at run time, maybe after quite a while of running, or maybe on a machine of someone else who uses your code. And that's not so nice. While with static typing, you have to specify all those types explicitly, but the types
are checked at compile time, which is nice. Of course, so there's another category of languages in here, untyped programming languages,
those are something completely else, let's not talk about them entirely. So, yeah, so like I said, Python is a dynamically typed language. Dynamic typing, we also call that duck typing, because if something walks like a duck and it quacks like a duck, then we just assume it is a duck.
So in other words, if some object that you put into a function has the required functionality, then we should just accept it. Why wouldn't we, right? So let's have an example here. So suppose that we have a class duck, and a duck can walk and quack, obviously, right?
So we can make an instance of a duck, and then we can make the duck walk and quack. That's awesome. Now, if we make a class donkey, and a donkey can certainly walk, but a donkey cannot quack, at least I haven't heard a donkey quack, then we can make another duck of type donkey, and
we can try to make this duck walk and quack, but when we try to do so, at run time, we will get an attribute error saying that the donkey object doesn't have an attribute that's named quack. Well, of course it's easy to see what goes on here, but in a more complex programme,
you might find out really late. So that is not so nice. This error, this is only really checked at run time.
So, now, if instead of a donkey, we have some kind of imposter duck, which isn't really a duck, but, well, it can walk and it can sort of try to quack, not quite quacking, then of course it's fine to just make a duck of type imposter duck and make it walk and make it quack.
The imposter duck behaves like a real duck, so we should just accept it as being a duck, right? I really like that about Python, you don't really have to worry about what the type is of something that you put in as an argument, you just make sure that it has the functionality or it behaves like the object that you would expect it
to do. So, then, as a wrap-up of this first part, Python is a dynamically typed language which, well, and I like that you don't have to, you don't really have to specify any type declarations, that's nice because you can just quickly code something together, gives
you lots of flexibility, you can create an imposter duck that behaves like a duck but actually isn't a duck, but as a downside, you don't have any type checking except at run time, and sometimes run time is just too late. So, in order to fix that last downside, we have typings, also called optional static
typing and it was introduced in Python 3.5 and usually that goes together with MyPy and MyPy is the optional static type checker for Python, so according to their description, they aim to combine the benefits of dynamic or duck typing
and static typing. You might know Python, you might know MyPy, sorry, you suppose, for an example of how to use typings, suppose that we have a class
duck and this duck can eat bread and it can swim. Now, then we can have a function called feed the duck, it takes a duck as argument and here after the argument name, I've specified a type hint, namely that I expect this duck to be of type duck.
And then it can have this duck eat bread. Well, that should work because the duck has a method that is called eat bread. Now, when we instantiate a duck, then we can just, we can call this function, we can use it on the duck and the duck is fed bread. Well, that's cool. Now then, if we have a monkey, a monkey can eat bananas and climb the tree,
if we can then try to feed a duck, feed the duck, use this feed the duck function on the monkey, we'll get an attribute error saying that the monkey object doesn't have any attribute called eat bread. Well, that's correct,
but that's at runtime. But if we would use MyPy beforehand, before runtime, it would already tell you that this argument one will feed the duck, so the only argument is actually of type monkey and it would expect it to be a duck. So here MyPy can really help you spot any issues with your code before runtime.
So that's cool. Now for a much more generic function, let's, so again, we have this, this class duck and the duck can eat bread and now we have a much more generic function that can feed bread to any animal and in this case,
we specified that this animal must be of type duck. That's cool, but now suppose that there is yet another animal, maybe a pig, and a pig can also eat bread. Then we would shoot, well, we really have to adapt the type hint of the feed bread function
to also accept a pig because otherwise MyPy will warn us that a pig is not a duck, which is true. So now here I've adapted the type hint for the animal argument to be of union, a union of a duck and a pig, basically saying
we either accept ducks or pigs. Now, this is all fine. This is perfect. But now suppose that this code is in some package that I download from PyPy. It's called animals and here is a class, mace.
Mace is my baby boy and he really likes to eat bread. He can also drink milk. His two favorite activities. Now, if I instantiate an instance of mace, excuse me, and then try to feed bread to mace, at runtime that would work fine
because mace has an eat bread method. But MyPy will warn us that this argument one, feed bread, has an incompatible type, mace, while expected a union of a duck or a pig. So while the function would work on this class mace,
you cannot have MyPy sort of accept this unless you can adapt the type hint of this function yourself. If you can't, if the function is in someone else's code,
then you cannot add any other class to this union in the type hint. So you're basically stuck. Of course you can have MyPy ignore this issue if you really know for sure that is fine. But, well, it's not so beautiful. So as a wrap-up for type hints, type declarations, they're optional.
That's really nice. So you don't have to, but you can. And when you do, you get optional static type checking. So before runtime, you get a warning when something is wrong. But you cannot adapt type hints of imported code. So if you make something that's compatible, well, you can tell the other library that you made something
that is compatible with them. So that's not so nice. Okay, let's move on. ABCs, abstract base classes. So what are they? Here's an example. So ABCs are base classes, so classes that you can inherit from,
but they cannot be instantiated. So typically they're used to define the interface of what subclasses should look like. And so here I have a base class called Animal, which has an abstract method, so a not-implemented method called Walk.
Now forgive me for assuming that all animals can walk. All animals that we'll use in this presentation can surely walk. Now we cannot instantiate an animal, because, well, an animal is a base class. It's abstract, we cannot use it.
So if we try to instantiate an animal at runtime, we'll get a type error saying that we cannot do that. What we can do, though, is we can define a class Duck that inherits from Animal and that can walk. And now we can instantiate the Duck, and this Duck is then an instance of Animal.
Well, okay, so that's cool. So how is this useful? Now suppose that we want to go back to the previous example with bread-eating. Then we can make a base class that's called EatBread that defines that all subclasses should have a method
that is called EatBread. And then we can define Duck and Pig both to inherit from this EatBread base class. And now in the FeedBread function, all we need to do is specify that this Animal should be of type EatBread
and then we know that Animal can eat bread. So now if I were to import this code from some package I found somewhere and use it with my baby boy, I would define his class to be this that inherits from EatBread,
and then I can define how he can eat bread and drink milk. And I can use an instance of Maze with this function. That works fine. Typically though, those base classes are not so easily exposed in packages that I tend to use. So then instead of having to import
or being able to import FeedBread and EatBread from Animals, you'd have to find this EatBread base class somewhere deep inside the library somewhere, like from Animals.Base.Eats import EatBread. But still, this works fine.
Alternatively, if you don't really want to inherit from the base class, you can explicitly register your class as being a subclass of an abstract base class. So that's what I do here. So I define the class Maze, which doesn't inherit from EatBread,
but then I register Maze as, well, a subclass of EatBread. So in this case, Maze still is, so an instance of Maze still is an instance of EatBread, although it didn't inherit any functionality that I may have defined in EatBread.
In this case, of course, I didn't define any functionality in the EatBread base class. I only defined that, well, an abstract method without any implementation. So, well, in this case, it's exactly the same as the previous example.
But in some cases, you might want to register a subclass without inheriting any functionality that has been defined already. So this all works fine, right? This is pretty beautiful. But sometimes you might still run into issues.
So suppose that there is this package animals that defines the base class animal and a dog that can walk and a function that can walk an animal. And then there's yet another package that's called llamas that defines a llama, and the llama can also walk, right?
llamas can walk. Now, if in our code we import this walkAnimal function from the animals package and then the llama object from the llamas package, then sure, at runtime, we can walk the llama. That's all right. But mypy will complain that llama
is actually not a subclass of an animal, which is true, because, well, they're from different packages. So why would it be? Of course, what you could do is fix that by registering llama to be a subclass of animal. But this feels kind of fishy to me
because you're really changing the internals of packages you're using, right? It works. It's not super nice. So wrap up for so far. Abstract base classes, they give a lot more structure to your types.
That's nice. Type hints in this way won't need updates for new subclasses, so you can add as many animals as you want, but you'll never have to update all those type hints that say animal. It's automatically included. But you may still, in some cases, have difficulties combining classes
from multiple libraries. Won't happen so often, and you might be able to be okay with that, but I don't think it's so nice. And those virtual subclasses, so those subclasses that don't inherit, that you can explicitly register, that's also nice, but you still need to explicitly register them,
which I also don't like so much. So now we'll come, finally, to protocols, also called structural subtyping, or static duct typing. Those were introduced in Python 3.8. And protocols make everything that we've encountered so far
much and much more beautiful. So let's have a look how. So a protocol is a special case of abstract base class. And here I define a class eatBread that is a protocol that defines, so this function, sorry, this method, eatBread,
without any implementation, and then I define a function feedBread that takes any animal as argument, and the animal should be of type eatBread so that the function can make the animal eatBread. Now if we define a class, so when we define a class called duck that has a method that is eatBread,
is called eatBread, then this is automatically and implicitly considered to be a subtype of eatBread, but only when typing. So this protocol automatically, so this class duck is automatically a subtype of the protocol
that defines what these classes that adhere to this protocol should look like. And as duck implements all the attributes and methods that this protocol defines, in this case it's just one, it is automatically a of type eatBread.
And so now if we call feedBread on the duck, that's okay, both at run time as well as when type checking. Now if I were to import that function from a package, so from animals import feedBread,
and then again define a class mace, then mace is also implicitly a subtype of eatBread because it implements a method that's called eatBread. And again I can call this function feedBread on mace, and that works all fine, without having to do anything at all.
All I need to do is make sure that the required functionality is there. So the class mace has a method that's called eatBread. That's all I need to do. And then everything works fine. That's pretty nice, isn't it? So actually before this there were already
some kind of protocols that, so before Python 3.8 there were some protocols that we were used to, like knowing and playing around with. So if you've already used some type hints with, for example, an iterable of a certain type, or an iterator of a certain type, or a size,
then in effect what happened was just checking that the object, whatever it was, in the case of iterable, it should have had the dunder iter method for an iterator, this were the dunder next and dunder iter methods for the size
is the dunder len. So now since Python 3.8 these are actual protocols, but before those, before that time those were sort of not really protocols, but behaved at least like those.
So now protocols are mainly were mainly designed to be used when type checking, so for mypy, so before you ever run your code, make sure that all those type hints that you use the functions according to the type hints. But you
can actually use them at runtime. On the other hand, so if you just do that like this, so we have this protocol that's called eatBread and it defines that all classes that are eatBread should have the method eatBread and we create the duck that can eatBread, then
a duck is not an instance of eatBread. It is only at when type checking. But if you really wanted to, you can make this happen by using this runtime checkable decorator. If you add this to your protocol, then suddenly
any object that adheres to the protocol becomes an instance of that protocol at runtime. Note that this isn't completely safe because at runtime this only checks the existence of the protocol
numbers, the protocol members, and their names and it doesn't check the signatures. So if you implement a method that is called eatBread that actually takes arguments it would still be an instance of eatBread
while type checking that wouldn't be the case. So it's not completely safe. But still, you can do something with this. Without ever having to explicitly specify that duck is of type eatBread. This just magically makes it work.
So, wrapping up. Protocols. As I said, I think this makes all of what we've seen so much more beautiful because there is no need to inherit or register anything. There are no more difficulties combining libraries because, well, it just implicitly makes it work as long as the right methods are defined.
If you want to use this at runtime make sure to decorate your protocol with the runtime checkable decorator and be aware that it's not completely safe although in practice you'll probably be fine. And so this, protocols,
gives us the best of both worlds. Static type checking whenever you want it and dynamic dynamically typed language like Python but also static checking of dynamic types with protocols. So, that brings me to the end. I hope
I've convinced you that you do need protocols. Thanks. Does anyone have any questions?
Do you want to come up to the microphone here? I have a question. What pitfalls do you see of using a prototype because you're using a lot of magic in the background.
I don't really think that there's a lot of magic in the background. so type checking in Python is just about comparing signatures, right? And in the case of protocols you just compare also
the existence of members and attributes of a class and you can just simply define what any object that you expect should look like instead of being explicitly like that object. Now, like I said when using this at runtime maybe you should be a little bit careful but
so for type checking I don't really see any pitfalls. Yeah, the runtime checking, does that also
go to the super command? I mean, it's a sort of implicit subtype but it won't be findable via super, right? It doesn't go to the init of the protocol class. No, I don't I assume not, to be honest I haven't worked with that I see people shaking their heads as you
don't define any functionality in a protocol, just the existence of attributes and methods there shouldn't be any functionality inside the initializer of the protocol anyway. And the protocol itself, is that an abstract thing or can you actually work with that? So the protocol exists at runtime
but as it hasn't, so there's no implementation of any of the methods or attributes, it wouldn't be very useful except for maybe if you want But doesn't throw this error like ABC classes? I've never tried to instantiate a protocol and it does throw, yeah?
Well, thanks for the answer. One more in-person question if anyone has another one
Thanks a bunch Is something a protocol if it has a property but the property returns the contributor or not implemented error? Oh wow, now we're really getting into the nitty-gritty details. I don't know. I would hope so but I don't know.
Thank you. Do we have any remote questions? Okay Alright, if that's it then please say thanks to our guest Thank you