Dead Simple Python (12 Part Series)
Classes and objects: the bread-and-butter of many a developer. Object-oriented programming is one of the mainstays of modern programming, so it shouldn't come as a surprise that Python is capable of it.
But if you've done object-oriented programming in any other language before coming to Python, I can almost guarantee you're doing it wrong.
Hang on to your paradigms, folks, it's going to be a bumpy ride.
Let's make a class, just to get our feet wet. Most of this won't come as a surprise to anyone.
class Starship(object): sound = "Vrrrrrrrrrrrrrrrrrrrrr" def __init__(self): self.engines = False self.engine_speed = 0 self.shields = True def engage(self): self.engines = True def warp(self, factor): self.engine_speed = 2 self.engine_speed *= factor @classmethod def make_sound(cls): print(cls.sound)
Once that's declared, we can create a new instance, or object, from this class. All of the member functions and variables are accessible using dot notation.
uss_enterprise = Starship() uss_enterprise.warp(4) uss_enterprise.engage() uss_enterprise.engines >>> True uss_enterprise.engine_speed >>> 8
Wow, Jason, I knew this was supposed to be 'dead simple', but I think you just put me to sleep.
No surprises there, right? But look again, not at what is there, but what isn't there.
You can't see it? Okay, let's break this down. See if you can spot the surprises before I get to them.
We start with the definition of the class itself:
Python might be considered one of the more truly object-oriented languages, on account of its design principle of "everything is an object." All other classes inherit from that
Of course, most Pythonistas really hate boilerplate, so as of Python 3, we can also just say this and call it good:
Personally, considering The Zen of Python's line about "Explicit is better than implicit," I like the first way. We could debate it until the cows come home, really, so let's just make it clear that both approaches do the same thing in Python 3, and move on.
Legacy Note: If you intend your code to work on Python 2, you must say
I'm going to jump down to this line...
def engage(self, factor):
Obviously, that's a member function or method. In Python, we pass
self as the first parameters to every single method. After that, we can have as many parameters as we want, the same as with any other function.
We actually don't have to call that first argument
self; it'll work the same regardless. But, we always use the name
self there anyway, as a matter of style. There exists no valid reason to break that rule.
"But, but...you literally just broke the rule yourself! See that next function?"
@classmethod def make_sound(cls):
You may remember that in object-oriented programming, a class method is one that is shared between all instances of the class (objects). A class method never touches member variables or regular methods.
If you haven't already noticed, we always access member variables in a class via the dot operator:
self.. So, to make it extra-super-clear we can't do that in a class method, we call the first argument
cls. In fact, when a class method is called, Python passes the class to that argument, instead of the object.
As before, we can call
cls anything we want, but that doesn't mean we should.
For a class method, we also MUST put the decorator
@classmethod on the line just above our function declaration. This tells the Python language that you're making a class method, and that you didn't just get creative with the name of the
Those methods above would get called something like this...
uss_enterprise = Starship() # Create our object from the starship class # Note, we aren't passing anything to 'self'. Python does that implicitly. uss_enterprise.warp(4) # We can call class functions on the object, or directly on the class. uss_enterprise.make_sound() Starship.make_sound()
Those last two lines will both print out "Vrrrrrrrrrrrrrrrrrrrrr" the exact same way. (Note that I referred to
cls.sound in that function.)
Come on, you know you made sound effects for your imaginary spaceships when you were a kid. Don't judge me.
The old adage is true: you don't stop learning until you're dead. Kinyanjui Wangonya pointed out in the comments that one didn't need to pass
cls to "static methods" - the phrase I was using in the first version of this article.
Turns out, he's right, and I was confused!
Unlike many other languages, Python distinguishes between static and class methods. Technically, they work the same way, in that they are both shared among all instances of the object. There's just one critical difference...
A static method doesn't access any of the class members; it doesn't even care that it's part of the class! Because it doesn't need to access any other part of the class, it doesn't need the
Let's contrast a class method with a static method:
@classmethod def make_sound(cls): print(cls.sound) @staticmethod def beep(): print("Beep boop beep")
beep() needs no access to the class, we can make it a static method by using the
@staticmethod decorator. Python won't implicitly pass the class to the first argument, unlike what it does on a class method (
Despite this difference, you call both the same way.
uss_enterprise = Starship() uss_enterprise.make_sound() >>> Vrrrrrrrrrrrrrrrrrrrrr Starship.make_sound() >>> Vrrrrrrrrrrrrrrrrrrrrr uss_enterprise.beep() >>> Beep boop beep Starship.beep() >>> Beep boop beep
Every Python class needs to have one, and only one,
__init__(self) function. This is called the initializer.
def __init__(self): self.engine_speed = 1 self.shields = True self.engines = False
If you really don't need an initializer, it is technically valid to skip defining it, but that's pretty universally considered bad form. In the very least, define an empty one...
def __init__(self): pass
While we tend to use it the same way as we would a constructor in C++ and Java,
__init__(self) is not a constructor! The initializer is responsible for initializing the instance variables, which we'll talk more about in a moment.
We rarely need to actually to define our own constructor. If you really know what you're doing, you can redefine the
def __new__(cls): return object.__new__(cls)
By the way, if you're looking for the destructor, that's the
In Python, our classes can have instance variables, which are unique to our object (instance), and class variables (a.k.a. static variables), which belong to the class, and are shared between all instances.
I have a confession to make: I spent the first few years of Python development doing this absolutely and completely wrong! Coming from other object-oriented languages, I actually thought I was supposed to do this:
class Starship(object): engines = False engine_speed = 0 shields = True def __init__(self): self.engines = False self.engine_speed = 0 self.shields = True def engage(self): self.engines = True def warp(self, factor): self.engine_speed = 2 self.engine_speed *= factor
The code works, so what's wrong with this picture? Read it again, and see if you can figure out what's happening.
Final Jeopardy music plays
Maybe this will make it obvious.
uss_enterprise = Starship() uss_enterprise.warp(4) print(uss_enterprise.engine_speed) >>> 8 print(Starship.engine_speed) >>> 0
Did you spot it?
Class variables are declared outside of all functions, usually at the top. Instance variables, on the other hand, are declared in the
__init__(self) function: for example,
self.engine_speed = 0.
So, in our little example, we've declared a set of class variables, and a set of instance variables, with the same names. When accessing a variable on the object, the instance variables shadow (hide) the class variables, making it behave as we might expect. However, we can see by printing
Starship.engine_speed that we have a separate class variable sitting in the class, just taking up space. Talk about redundant.
Anyone get that right? Sloan did, and wagered...ten thousand cecropia leaves. Looks like the sloth is in the lead. Amazingly enough.
By the way, you can declare instance variables for the first time from within any instance method, instead of the initializer. However...you guessed it: don't. The convention is to ALWAYS declare all your instance variables in the initializer, just to prevent something weird from happening, like a function attempting to access a variable that doesn't yet exist.
If you come from another object-oriented language, such as Java and C++, you're also probably in the habit of thinking about scope (private, protected, public) and its traditional assumptions: variables should be private, and functions should (usually) be public. Getters and setters rule the day!
I'm also an expert in C++ object-oriented programming, and I have to say that I consider Python's approach to the issue of scope to be vastly superior to the typical object-oriented scope rules. Once you grasp how to design classes in Python, the principles will probably leak into your standard practice in other languages...and I firmly believe that's a good thing.
Ready for this? Your variables don't actually need to be private.
Yes, I just heard the gurgling scream of the Java nerd in the back. "But...but...how will I keep developers from just tampering with any of the object's instance variables?"
Often, that concern is built on three flawed assumptions. Let's set those right first:
The developer using your class almost certainly isn't in the habit of modifying member variables directly, any more than they're in the habit of sticking a fork in a toaster.
If they do stick a fork in the toaster, proverbially speaking, the consequences are on them for being idiots, not on you.
As my Freenode #python friend
grymonce said, "if you know why you aren't supposed to remove stuck toast from the toaster with a metal object, you're allowed to do so."
In other words, the developer who is using your class probably knows better than you do about whether they should twiddle the instance variables or not.
Now, with that out of the way, we approach an important premise in Python: there is no actual 'private' scope. We can't just stick a fancy little keyword in front of a variable to make it private.
What we can do is stick an underscore at the front of the name, like this:
That underscore isn't magical. It's just a warning label to anyone using your class: "I recommend you don't mess with this. I'm doing something special with it."
Now, before you go sticking
_ at the start of all your instance variable names, think about what the variable actually is, and how you use it. Will directly tweaking it really cause problems? In the case of our example class, as it's written right now, no. This actually would be perfectly acceptable:
uss_enterprise.engine_speed = 6 uss_enterprise.engage()
Also, notice something beautiful about that? We didn't write a single getter or setter! In any language, if a getter or setter are functionally identical to modifying the variable directly, they're an absolute waste. That philosophy is one of the reasons Python is such a clean language.
You can also use this naming convention with methods you don't intend to be used outside of the class.
Side Note: Before you run off and go eschew
protected from your Java and C++ code, please understand that there's a time and a place for scope. The underscore convention is a social contract among Python developers, and most languages don't have anything like that. So, if you're in a language with scope, use
protected on any variable you would have put an underscore in front of in Python.
Now, on a very rare occasion, you may have an instance variable which absolutely, positively, never, ever should be directly modified outside of the class. In that case, you may precede the name of the variable with two underscores (
__), instead of one.
This doesn't actually make it private; rather, it performs something called name mangling: it changes the name of the variable, adding a single underscore and the name of the class on the front.
In the case of
class Starship, if we were to change
self.__shields, it would be name mangled to
So, if you know how that name mangling works, you can still access it...
uss_enterprise = Starship() uss_enterprise._Starship__shields >>> True
It's important to note, you also cannot have more than one trailing underscore if this is to work. (
__foo_ will be mangled, but
__foo__ will not). But then, PEP 8 generally discourages trailing underscores, so it's kinda a moot point.
By the way, the purpose of the double underscore (
__) name mangling actually has nothing to do with private scope; it's all about preventing name conflicts with some technical scenarios. In fact, you'll probably get a few serious frowns from Python ninjas for employing
__ at all, so use it sparingly.
As I said earlier, getters and setters are usually pointless. On occasion, however, they have a purpose. In Python, we can use properties in this manner, as well as to pull off some pretty nifty tricks!
Properties are defined simply by preceding a method with
My favorite trick with properties is to make a method look like an instance variable...
class Starship(object): def __init__(self): self.engines = True self.engine_speed = 0 self.shields = True @property def engine_strain(self): if not self.engines: return 0 elif self.shields: # Imagine shields double the engine strain return self.engine_speed * 2 # Otherwise, the engine strain is the same as the speed return self.engine_speed
When we're using this class, we can treat
engine_strain as an instance variable of the object.
uss_enterprise = Starship() uss_enterprise.engine_strain >>> 0
Beautiful, isn't it?
(Un)fortunately, we cannot modify
engine_strain in the same manner.
uss_enterprise.engine_strain = 10 >>> Traceback (most recent call last): >>> File "<stdin>", line 1, in <module> >>> AttributeError: can't set attribute
In this case, that actually does make sense, but it might not be what you're wanting other times. Just for fun, let's define a setter for our property too; at least one with nicer output than that scary error.
@engine_strain.setter def engine_strain(self, value): print("I'm giving her all she's got, Captain!")
We precede our method with the decorator
@NAME_OF_PROPERTY.setter. We also have to accept a single
value argument (after
self, of course), and positively nothing beyond that. You'll notice we're not actually doing anything with the
value argument in this case, and that's fine for our example.
uss_enterprise.engine_strain = 10 >>> I'm giving her all she's got, Captain!
That's much better.
As I mentioned earlier, we can use these as getters and setters for our instance variables. Here's a quick example of how:
class Starship: def __init__(self): # snip self._captain = "Jean-Luc Picard" @property def captain(self): return self._captain @captain.setter def captain(self, value): print("What do you think this is, " + value + ", the USS Pegasus? Back to work!")
We simply preceded the variable these functions concern with an underscore, to indicate to others that we intend to manage the variable ourselves. The getter is pretty dull and obvious, and is only needed to provide expected behavior. The setter is where things are interesting: we knock down any attempted mutinies. There will be no changing this captain!
uss_enterprise = Starship() uss_enterprise.captain >>> 'Jean-Luc Picard' uss_enterprise.captain = "Wesley" >>> What do you think this is, Wesley, the USS Pegasus? Back to work!
Technical rabbit trail: if you want to create class properties, that requires some hacking on your part. There are several solutions floating around the net, so if you need this, go research it!
A few of the Python nerds will be on me if I didn't point out, there is another way to create a property without the use of decorators. So, just for the record, this works too...
class Starship: def __init__(self): # snip self._captain = "Jean-Luc Picard" def get_captain(self): return self._captain def set_captain(self, value): print("What do you think this is, " + value + ", the USS Pegasus? Back to work!") captain = property(get_captain, set_captain)
(Yes, that last line exists outside of any function.)
As usual, the documentation on properties has additional information, and some more nifty tricks with properties.
Finally, we come back to that first line for another look.
Remember why that
(object) is there? We're inheriting from Python's
object class. Ahh, inheritance! That's where it belongs.
class USSDiscovery(Starship): def __init__(self): super().__init__() self.spore_drive = True self._captain = "Gabriel Lorca"
The only real mystery here is that
super().__init__() line. In short,
super() refers to the class we inherited from (in this case,
Starship), and calls its initializer. We need to call this, so
USSDiscovery has all the same instance variables as
Of course, we can define new instance variables (
self.spore_drive), and redefine inherited ones (
We could have actually just called that initializer with
Starship.__init__(), but then if we wanted to change what we inherit from, we'd have to change that line too. The
super().__init__() approach is ultimately just cleaner and more maintainable.
Legacy Note: By the way, if you're using Python 2, that line is a little uglier:
Before you ask: YES, you can do multiple inheritance with
class C(A, B):. It actually works better than in most languages! Regardless, but you can count on a side order of headaches, especially when using
As you can see, Python classes are a little different from other languages, but once you're used to them, they're actually a bit easier to work with.
But if you've coded in class-heavy languages like C++ or Java, and are working on the assumption that you need classes in Python, I have a surprise for you. You really aren't required to use classes at all!
Classes and objects have exactly one purpose in Python: data encapsulation. If you need to keep data and the functions for manipulating it together in a handy unit, classes are the way to go. Otherwise, don't bother! There's absolutely nothing wrong with a module composed entirely of functions.
Whew! You still with me? How many of those surprises about classes in Python did you guess?
__init__(self)function is the initializer, and that's where we do all of our variable initialization.
Methods (member functions) must take
selfas their first argument.
Class methods must take
clsas their first argument, and have the decorator
@classmethodon the line just above the function definition. They can access class variables, but not instance variables.
Static methods are similar to class methods, except they don't take
clsas their first argument, and are preceded by the decorator
@staticmethod. They cannot access any class or instance variables or functions. They don't even know they're part of a class.
Instance variables (member variables) should be declared inside
__init__(self)first. We don't declare them outside of the constructor, unlike most other object-oriented languages.
Class variables or static variables are declared outside of any function, and are shared between all instances of the class.
There are no private members in Python! Precede a member variable or a method name with an underscore (
_) to tell developers they shouldn't mess with it.
If you precede a member variable or method name with two underscores (
__), Python will change its name using name mangling. This is more for preventing name conflicts than hiding things.
You can make any method into a property (it looks like a member variable) by putting the decorator
@propertyon the line above its declaration. This can also be used to create getters.
You can create a setter for a property (e.g.
foo) by putting the decorator
@foo.setterabove a function
A class (e.g.
Dog) can inherit from another class (e.g.
Animal) in this manner:
class Dog(Animal):. When you do this, you should also start your initializer with the line
super().__init__()to call the initializer of the base class.
Multiple inheritance is possible, but it might give you nightmares. Handle with tongs.
As usual, I recommend you read the docs for more:
- Python Tutorials: Classes
- Python Reference: Built-In Functions - @classmethod
- Python Reference: Built-In Functions - @staticmethod
- Python Reference: Built-In Functions - @property
Ready to go write some Python classes? Make it so!
Thank you to
grym (Freenode IRC
#python), and @wangonya (Dev) for suggested revisions.