DEV Community

Cover image for Dead Simple Python: Classes
Jason C. McDonald
Jason C. McDonald

Posted on • Edited on

Dead Simple Python: Classes

Like the articles? Buy the book! Dead Simple Python by Jason C. McDonald is available from No Starch Press.


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.

Class Is In Session

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Declaration

We start with the definition of the class itself:

class Starship(object):
Enter fullscreen mode Exit fullscreen mode

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 object class.

Of course, most Pythonistas really hate boilerplate, so as of Python 3, we can also just say this and call it good:

class Starship:
Enter fullscreen mode Exit fullscreen mode

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 (object).

Methods

I'm going to jump down to this line...

def warp(self, factor):
Enter fullscreen mode Exit fullscreen mode

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):
Enter fullscreen mode Exit fullscreen mode

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 self argument.

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()
Enter fullscreen mode Exit fullscreen mode

Those last two lines will both print out "Vrrrrrrrrrrrrrrrrrrrrr" the exact same way. (Note that I referred to cls.sound in that function.)

...

What?

Come on, you know you made sound effects for your imaginary spaceships when you were a kid. Don't judge me.

Class vs Static Methods

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 cls argument.

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")
Enter fullscreen mode Exit fullscreen mode

Because 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 (make_sound())

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
Enter fullscreen mode Exit fullscreen mode

Initializers and Constructors

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 __new__(cls) function...

def __new__(cls):
    return object.__new__(cls)
Enter fullscreen mode Exit fullscreen mode

By the way, if you're looking for the destructor, that's the __del__(self) function.

Variables

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Scope: Private and Public

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 grym once 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: self._engine.

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()
Enter fullscreen mode Exit fullscreen mode

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 private and 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 private or protected on any variable you would have put an underscore in front of in Python.

Private...Sort Of

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 to self.__shields, it would be name mangled to self._Starship__shields.

So, if you know how that name mangling works, you can still access it...

uss_enterprise = Starship()
uss_enterprise._Starship__shields
>>> True
Enter fullscreen mode Exit fullscreen mode

It's important to note, you also cannot have more than one trailing underscore if this is to work. (__foo and __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.

Properties

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 @property.

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!")
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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!")
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

(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.

Inheritance

Finally, we come back to that first line for another look.


class Starship(object):
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 Starship.

Of course, we can define new instance variables (self.spore_drive), and redefine inherited ones (self._captain).

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: super(USSDiscovery, self).__init__().

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 super().

Hold the Classes!

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.

Review

Whew! You still with me? How many of those surprises about classes in Python did you guess?

Let's review...

  • The __init__(self) function is the initializer, and that's where we do all of our variable initialization.

  • Methods (member functions) must take self as their first argument.

  • Class methods must take cls as their first argument, and have the decorator @classmethod on 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 cls as 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 @property on 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.setter above a function foo.

  • 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:

Ready to go write some Python classes? Make it so!


Thank you to deniska, grym (Freenode IRC #python), and @wangonya (Dev) for suggested revisions.

Top comments (23)

Collapse
 
rpalo profile image
Ryan Palo

I read a really funny analogy for why python’s private-ish variables are good enough and we don’t have stricter access control:

I’d like you to stay out of my living room because I ask you to, not because I have a shotgun!

Thanks for the article!

Collapse
 
akashsharma02 profile image
Akash Sharma • Edited

I'd like to add that you should mention that there's pretty much never a good reason to use staticmethod as opposed to a global method in the module.

Usually, when we write a module, we can simply write a method outside the class, to get the same behaviour as a staticmethod

Collapse
 
codemouse92 profile image
Jason C. McDonald

I try to avoid such statements, especially in this article series. If it exists, it exists for some reason. ;)

That said, I plan to discuss the ins and outs of this in more depth in the book.

Collapse
 
akashsharma02 profile image
Akash Sharma

Ah I see! I really appreciate this series. It is very well written and enjoyable to read!

By the way, what is that reason?

Thread Thread
 
codemouse92 profile image
Jason C. McDonald

Namespacing.

Collapse
 
paulboot profile image
Paul Boot

Thanks for the excellent Article.

Some small suggestion: in the setter example the actual line of code that sets the Instance Member self.__captain = value is missing ;-)

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!")

uss_enterprise = Starship()
print(uss_enterprise.captain)
uss_enterprise.captain = "Wesley"
print(uss_enterprise.captain)

Resulting in this output, Jean-Luc Picard refuses to go away....

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!
uss_enterprise.captain
>>> 'Jean-Luc Picard'
Collapse
 
wangonya profile image
Kelvin Wangonya • Edited

@staticmethod
def make_sound(cls):

Should cls be passed into a staticmethod? I thought that was only for class methods

Collapse
 
codemouse92 profile image
Jason C. McDonald • Edited

I cannot believe it! I actually missed that little detail: @staticmethod and @classmethod are actually two separate things!

I've just edited the article to correct that, and added a new section to clarify the difference. Thank you so much for correcting that.

Collapse
 
wangonya profile image
Kelvin Wangonya

Great article!

Collapse
 
ardunster profile image
Anna R Dunster

Nice, properties, getters, and setters all explain some things I had never even seen or thought of before (Python being my first programming language), it definitely expanded my understanding of what a class is and can do.

Collapse
 
johnlukeg profile image
John Luke Garofalo

Great article! Python is neither my primary nor my secondary language, but I've been using it a lot for my grad school work. I realize how naive my own Python code has been after reading your post haha. Do you have any suggestions for more material like this to learn the best practices? I have found that the python docs are not the most captivating. Thank you for this post! Great job.

Collapse
 
codemouse92 profile image
Jason C. McDonald

Unfortunately, I haven't found much! That's precisely why I started this article series.

If you have some extra cash laying around, you could sign up for Udemy: The Complete Python Course by Codestars. He covers good practice pretty well. However, it is unfortunately quite beginner-oriented, so it's slow going at first.

Collapse
 
natter1 profile image
natter1
def engage(self, factor):

in the subtopic Methods should be

def warp(self, factor):

as engage has no parameter factor

Collapse
 
codemouse92 profile image
Jason C. McDonald

Another great catch. Thank you!

Collapse
 
ravin309 profile image
Ravinder Kumar

In inheritance, what if we do not write 'super().init()' in child class' init() method? Does child class not call it by default?

Collapse
 
codemouse92 profile image
Jason C. McDonald

No, it doesn't, as far as I know. As mentioned in the Zen of Python, Explicit is better than implicit. It's part of the language philosophy.

We often need to handle passing data to the parent __init__() in a specific way, and we may need to do some other things first, so Python just assumes we know best how to handle that.

Imagine how maddening it could get if Python sometimes called it for you, but sometimes not! (Some languages actually are confusing like that, but Python aims to be obvious.)

Collapse
 
ravin309 profile image
Ravinder Kumar

Thanks for clarifying!
It would be very helpful if you also explain MRO (Method Resolution Order) in context of Python 3 in any upcoming series.

Collapse
 
tinsoldier6 profile image
Jason Gade

This was a very clear and well-written article, probably one of the best that I’ve read regarding Python classes. Thanks!

Collapse
 
rupeshtiwari profile image
Rupesh Tiwari

Nice article Jason. Keep writing more.