Recommended Video Course
Inheritance and Internals: Object-Oriented Programming in Python
Table of Contents
Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding:Inheritance and Internals: Object-Oriented Programming in Python
Python classes form the backbone of object-oriented programming, enabling you to encapsulate data and behavior into a single entity. When you work with a Python class, you define attributes to store data and methods to perform actions. This structure allows you to model real-world objects and create organized, reusable code.
A class in Python serves as a blueprint for creating objects, which are instances of the class. You use classes when you need to encapsulate related data and functions, making your code modular and easier to manage. By defining classes, you can create multiple objects that share the same attributes and methods, while maintaining their own unique state.
In this tutorial, you’ll learn how to define and use Python classes, understand the distinction between classes and objects, and explore methods and attributes. You’ll also learn about instance and class attributes, methods, inheritance, and common pitfalls to avoid when working with classes.
By the end of this tutorial, you’ll understand that:
class
keyword, andinstantiate them to create objects.To get the most out of this tutorial, you should know about Pythonvariables,data types, andfunctions. Some experience withobject-oriented programming (OOP) is also a plus. Don’t worry if you’re not an OOP expert yet. In this tutorial, you’ll learn the key concepts that you need to get started and more. You’ll also write several practical examples to help reinforce your knowledge of Python classes.
Get Your Code:Click here to download your free sample code that shows you how to build powerful object blueprints with classes in Python.
Take the Quiz: Test your knowledge with our interactive “Python Classes - The Power of Object-Oriented Programming” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Python Classes - The Power of Object-Oriented ProgrammingIn this quiz, you'll test your understanding of Python classes. With this knowledge, you'll be able to define reusable pieces of code that encapsulate data and behavior in a single entity, model real-world objects, and solve complex problems.
Python is amultiparadigm programming language that supportsobject-oriented programming (OOP) through classes that you can define with theclass
keyword. You can think of a class as a piece of code that specifies thedata andbehavior that represent and model a particular type of object.
What is a class in Python? A common analogy is that a class is like the blueprint for a house. You can use the blueprint to create several houses and even a complete neighborhood. Each concrete house is anobject orinstance that’s derived from the blueprint.
Each instance can have its own properties, such as color, owner, and interior design. These properties carry what’s commonly known as the object’sstate. Instances can also have different behaviors, such as locking the doors and windows, opening the garage door, turning the lights on and off, watering the garden, and more.
In OOP, you commonly use the termattributes to refer to the properties or data associated with a specific object of a given class. In Python, attributes arevariables defined inside a class with the purpose of storing all the required data for the class to work.
Similarly, you’ll use the termmethods to refer to the different behaviors that objects will show. Methods are functions that you define within a class. These functions typically operate on or with the attributes of the underlying instance or class. Attributes and methods are collectively referred to asmembers of a class or object.
You can write classes to model the real world. These classes will help you better organize your code and solve complex programming problems.
For example, you can use classes to create objects that emulate people, animals, vehicles, books, buildings, cars, or other objects. You can also model virtual objects, such as a web server,directory tree, chatbot, file manager, and more.
Finally, you can use classes to buildclass hierarchies. This way, you’ll promote code reuse and remove repetition throughout your codebase.
In this tutorial, you’ll learn a lot about classes and all the cool things that you can do with them. To kick things off, you’ll start by defining your first class in Python. Then you’ll dive into other topics related to instances, attributes, and methods.
To define a class, you need to use theclass
keyword followed by the class name and a colon, just like you’d do for othercompound statements in Python. Then you must define the class body, which will start at the next indentation level:
classClassName:<body>
In a class’s body, you can define attributes and methods as needed. As you already learned, attributes are variables that hold the class data, while methods are functions that provide behavior and typically act on the class data.
Note: In Python, the body of a given class works as anamespace where attributes and methods live. You can only access those attributes and methods through the class or its objects.
As an example of how to define attributes and methods, say that you need aCircle
class to model different circles in a drawing application. Initially, your class will have a single attribute to hold the radius. It’ll also have a method to calculate the circle’s area:
circle.py
importmathclassCircle:def__init__(self,radius):self.radius=radiusdefcalculate_area(self):returnmath.pi*self.radius**2
In this code snippet, you defineCircle
using theclass
keyword. Inside the class, you write two methods. The.__init__()
method has a special meaning in Python classes. This method is known as the objectinitializer because it defines and sets the initial values for the object’s attributes. You’ll learn more about this method in theInstance Attributes section.
The second method ofCircle
is conveniently named.calculate_area()
and will compute the area of a specific circle by using its radius. In this example, you’ve used themath
module to access thepi
constant as it’s defined in that module.
It’s common for method names to contain a verb, such ascalculate, to describe an action the method performs. To learn more about naming functions and methods in Python, check out theHow Do You Choose Python Function Names? tutorial.
Note: In Python, the first argument of most methods isself
. This argument holds a reference to the current object so that you can use it inside the class. You’ll learn more about this argument in the section oninstance methods withself
.
Cool! You’ve written your first class. Now, how can you use this class in your code to represent several concrete circles? Well, you need to instantiate your class to create specific circle objects from it.
The action of creating concrete objects from an existing class is known asinstantiation. With every instantiation, you create a new object of the target class. To get your hands dirty, go ahead and make a couple of instances ofCircle
by running the following code in a PythonREPL session:
>>>fromcircleimportCircle>>>circle_1=Circle(42)>>>circle_2=Circle(7)>>>circle_1<__main__.Circle object at 0x102b835d0>>>>circle_2<__main__.Circle object at 0x1035e3910>
To create an object of a Python class likeCircle
, you must call theCircle()
class constructor with a pair of parentheses and a set of appropriate arguments. What arguments? In Python, the class constructor accepts the same arguments as the.__init__()
method. In this example, theCircle
class expects theradius
argument.
Calling the class constructor with different argument values will allow you to create different objects or instances of the target class. In the above example,circle_1
andcircle_2
are separate instances ofCircle
. In other words, they’re two different and concrete circles, as you can conclude from the code’s output.
Great! You already know how to create objects of an existing class by calling the class constructor with the required arguments. Now, how can you access the attributes and methods of a given class? That’s what you’ll learn in the next section.
In Python, you can access the attributes and methods of an object by usingdot notation with thedot operator. The following snippet of code shows the required syntax:
obj.attribute_nameobj.method_name()
Note that the dot (.
) in this syntax basically meansgive me the following attribute or method from this object. The first line returns the value stored in the target attribute, while the second line accesses the target method and calls it.
Note: Remember that to call a function or method, you need to use a pair of parentheses and a series of arguments, if applicable.
Now get back to your circle objects and run the following code:
>>>fromcircleimportCircle>>>circle_1=Circle(42)>>>circle_2=Circle(7)>>>circle_1.radius42>>>circle_1.calculate_area()5541.769440932395>>>circle_2.radius7>>>circle_2.calculate_area()153.93804002589985
In the first couple of lines after the instantiation, you access the.radius
attribute on yourcircle_1
object. Then you call the.calculate_area()
method to calculate the circle’s area. In the second pair of statements, you do the same but on thecircle_2
object.
You can also use dot notation and theassignment operator to change the current value of an attribute:
>>>circle_1.radius=100>>>circle_1.radius100>>>circle_1.calculate_area()31415.926535897932
Now the radius ofcircle_1
is entirely different. When you call.calculate_area()
, the result immediately reflects this change. You’ve changed the object’s internalstate or data, which typically impacts its behaviors or methods.
Before continuing diving into classes, you’ll need to be aware of some important naming conventions that Python uses in the context of classes. Python is a flexible language that loves freedom and doesn’t like to have hard restrictions. Because of that, the language and the community rely on conventions rather than restrictions.
Note: Most Python programmers follow thesnake_case naming convention, which involves using underscores (_
) to separate multiple words. This is used for functions and methods. However, the recommended naming convention for Python classes is thePascalCase, where each word is capitalized.
In the following two sections, you’ll learn about two important naming conventions that apply to class attributes.
The first naming convention that you need to know about is related to the fact that Python doesn’t distinguish betweenprivate,protected, andpublic attributes likeJava and other languages do. In Python, all attributes are accessible in one way or another.
However, Python has a well-established naming convention that you should use to communicate that an attribute or method isn’t intended for use from outside its containing class or object. The naming convention consists of adding aleading underscore to the member’s name. So, in a Python class, you’ll have the following conventions:
Member | Naming | Examples |
---|---|---|
Public | Use the normal naming pattern. | radius ,calculate_area() |
Non-public | Include a leading underscore in names. | _radius ,_calculate_area() |
Public members are part of the officialinterface orAPI (application programming interface) of your classes, whilenon-public members aren’t intended to be part of that API. This means that you shouldn’t use non-public members outside their defining class.
It’s important to note that the second naming convention onlyindicates that the attribute isn’t intended to be used directly from outside the containing class. It doesn’t prevent direct access, though. For example, you can run something likeobj._name
to access the content of._name
. However, this is bad practice, and you should avoid it.
Non-public members exist only to support the internal implementation of a given class and the owner of the class may remove them at any time, so you shouldn’t rely on such attributes and methods. The existence of these members depends on how the class is implemented. So, you shouldn’t use them directly in client code. If you do, then your code could break in the future.
When writing classes, sometimes it’s hard to decide if an attribute should be public or non-public. This decision will depend on how you want your users to use your classes. In most cases, attributes should be non-public to guarantee the safe use of your classes. A good approach will be to start with all your attributes as non-public and only make them public if real use cases appear.
Another naming convention that you can see and use in Python classes is to add two leading underscores to attribute and method names. This naming convention triggers what’s known asname mangling.
Name mangling is an automatic name transformation that prepends the class’s name to the member’s name, like in_ClassName__attribute
or_ClassName__method
. This transformation results in name hiding. In other words, mangled names aren’t available for direct access and aren’t part of a class’s public API.
For example, consider the following sample class:
>>>classSampleClass:...def__init__(self,value):...self.__value=value...def__method(self):...print(self.__value)...>>>sample_instance=SampleClass("Hello!")>>>vars(sample_instance){'_SampleClass__value': 'Hello!'}>>>vars(SampleClass)mappingproxy({ ... '__init__': <function SampleClass.__init__ at 0x105dfd4e0>, '_SampleClass__method': <function SampleClass.__method at 0x105dfd760>, '__dict__': <attribute '__dict__' of 'SampleClass' objects>, ...})>>>sample_instance=SampleClass("Hello!")>>>sample_instance.__valueTraceback (most recent call last):...AttributeError:'SampleClass' object has no attribute '__value'>>>sample_instance.__method()Traceback (most recent call last):...AttributeError:'SampleClass' object has no attribute '__method'
In this class,.__value
and.__method()
have two leading underscores, so their names are mangled to._SampleClass__value
and._SampleClass__method()
, as you can see in the highlighted lines. Python has automatically added the prefix_SampleClass
to both names. Because of this internal renaming, you can’t access the attributes from outside the class using their original names. If you try to do it, then you get anAttributeError
.
Note: In the above example, you use the built-invars()
function, which returns a dictionary of all the members associated with the given object. This dictionary plays an important role in Python classes. You’ll learn more about it in the.__dict__
attribute section.
The internal behavior described above hides the names, creating the illusion of a private attribute or method. However, they’re not strictly private. You can access them through their mangled names:
>>>sample_instance._SampleClass__value'Hello!'>>>sample_instance._SampleClass__method()Hello!
It’s still possible to access name-mangled attributes or methods using their mangled names, although this is bad practice, and you should avoid it in your code. If you see a name that uses this convention in someone’s code, then don’t try to force the code to use the name from outside its containing class.
Note: Name mangling is useful when you want to prevent name clashes in inheritance. To learn more about this topic, check out the tutorialDouble Leading Underscore in Classes: Python’s Name Mangling.
Wow! Up to this point, you’ve learned the basics of Python classes and a bit more. You’ll dive deeper into how Python classes work in a moment. But first, it’s time to discuss some reasons why you should learn about classes and use them in your Python projects.
Is it worth using classes in Python? Absolutely! Classes are the building blocks of object-oriented programming in Python. They allow you to leverage the power of Python while writing and organizing your code. By learning about classes, you’ll be able to take advantage of all the benefits that they provide. With classes, you can:
Model and solve complex real-world problems: You’ll find many situations where the objects in your code map to real-world objects. This can help you think about complex problems, which will result in better solutions to your programming problems.
Reuse code and avoid repetition: You can define hierarchies of related classes. The base classes at the top of a hierarchy provide common functionality that you can reuse later in the subclasses down the hierarchy. This allows you to reduce code duplication and promote code reuse.
Encapsulate related data and behaviors in a single entity: You can use Python classes to bundle together related attributes and methods in a single entity, the object. This helps you better organize your code using modular and autonomous entities that you can even reuse across multiple projects.
Abstract away the implementation details of concepts and objects: You can use classes to abstract away the implementation details of core concepts and objects. This will help you provide your users with intuitiveinterfaces (APIs) to process complex data and behaviors.
Unlock polymorphism with common interfaces: You can implement a particular interface in several slightly different classes and use them interchangeably in your code. This will make your code more flexible and adaptable.
In short, Python classes can help you write more organized, structured, maintainable, reusable, flexible, and user-friendly code. They’re a great tool to have under your belt. However, don’t be tempted to use classes for everything in Python. In some situations, they can overcomplicate your solutions.
Note: In Python, the public attributes and methods of a class make up what you’ll know as the class’sinterface orapplication programming interface (API). You’ll learn more about interfaces throughout this tutorial. Stay tuned!
In the following section, you’ll explore some situations where you should avoid using classes in your code.
Python classes are pretty cool and powerful tools that you can use in multiple scenarios. Because of this, some people tend to overuse classes and solve all their coding problems using them. However, sometimes using a class isn’t the best solution. Sometimes, amodule with a couple of functions is enough.
In practice, you’ll encounter a few situations in which you should avoid classes. For example, you shouldn’t use regular classes when you need to:
Data classes, enumerations, and named tuples are specially designed to store data. So, they might be the best solution if your class doesn’t have any behavior attached.
If your class has a single method in its API, then you may not require a class. Instead, use a function unless you need to retain a certain state between calls. If more methods appear later, then you can always create a class. Remember this principle from theZen of Python:
Simple is better than complex. (Source)
Additionally, you should avoid creating custom classes to wrap up functionality that’s available through built-in types or third-party classes. Use the type or third-party class directly.
You’ll find many other situations where you may not need to use classes in your Python code. For example, classes may be unnecessary when you’re working with:
You may find yourself in many other situations where using classes will be overkill. Classes are great, but don’t turn them into a one-size-fits-all type of tool. Start your code as simply as possible. If the need for a class appears, then go for it.
As you’ve learned, classes are great when you must bundle data and behavior together in a single entity. The data will come in the form of attributes, while the behavior will come as methods. You already have an idea of what an attribute is. Now it’s time to dive deeper into how you can add, access, and modify attributes in your custom classes.
First, you need to know that your classes can have two types of attributes in Python:
self
argument and dot notation, like inself.attribute = value
. Instance attributes belong to a concrete instance of a given class. Their data is only available to that instance and defines its state.Both types of attributes have their specific use cases. Instance attributes are, by far, the most common type of attribute that you’ll use in your day-to-day coding, but class attributes also come in handy.
Class attributes are variables that you define directly in the class body but outside of any method. These attributes are tied to the class itself rather than to particular objects of that class.
All the objects that you create from a particular class share the same class attributes with the same original values. Because of this, if you change a class attribute, then that change affects all the derived objects.
As an example, say that you want to create a class that keeps an internal count of the instances you’ve created. In that case, you can use a class attribute:
>>>classObjectCounter:...num_instances=0...def__init__(self):...ObjectCounter.num_instances+=1...>>>ObjectCounter()<__main__.ObjectCounter object at 0x10392d810>>>>ObjectCounter()<__main__.ObjectCounter object at 0x1039810d0>>>>ObjectCounter()<__main__.ObjectCounter object at 0x10395b750>>>>ObjectCounter()<__main__.ObjectCounter object at 0x103959810>>>>ObjectCounter.num_instances4>>>counter=ObjectCounter()>>>counter.num_instances5
ObjectCounter
keeps a.num_instances
class attribute that works as a counter of instances. When Python parses this class, it initializes the counter to zero and leaves it alone. Creating instances of this class means automatically calling the.__init__()
method and incremementing.num_instances
by one.
Note: In the above example, you’ve used the class name to access.num_instances
inside.__init__()
. However, using the built-intype()
function is best because it’ll make your class more flexible:
>>>classObjectCounter:...num_instances=0...def__init__(self):...type(self).num_instances+=1...
The built-intype()
function returns the class or type ofself
, which isObjectCounter
in this example. This subtle change makes your class more robust and reliable by avoiding hard-coding the name of the class that provides the attribute.
It’s important to note that you canaccess class attributes using either the class or one of its instances. That’s why you can use thecounter
object to retrieve the value of.num_instances
. However, if you need tomodify a class attribute, then you must use the class itself rather than one of its instances.
For example, if you useself
to modify.num_instances
, then you’ll be overriding the original class attribute by creating a new instance attribute:
>>>classObjectCounter:...num_instances=0...def__init__(self):...self.num_instances+=1...>>>ObjectCounter()<__main__.ObjectCounter object at 0x103987550>>>>ObjectCounter()<__main__.ObjectCounter object at 0x1039c5890>>>>ObjectCounter()<__main__.ObjectCounter object at 0x10396a890>>>>ObjectCounter()<__main__.ObjectCounter object at 0x1036fa110>>>>ObjectCounter.num_instances0
You can’t modify class attributes through instances of the containing class. Doing that will create new instance attributes with the same name as the original class attributes. That’s whyObjectCounter.num_instances
returns0
in this example. You’ve overridden the class attribute in the highlighted line.
Note: It’s curious what Python does in the example above. It uses the initial value of.num_instances
as a class attribute to create a new instance attribute with the same name and increments it by one. How come? Well, the lineself.num_instances += 1
is equivalent to the following assignment:
self.num_instances=self.num_instances+1
In this statement, the attribute on the left of the equal sign is an instance attribute, while the attribute on the right side of the sign is a class attribute. Otherwise, the+=
operator would’ve raised aNameError
.
In general, you should use class attributes for sharing data between instances of a class. Any changes on a class attribute will be visible to all the instances of that class.
Instance attributes are variables tied to a particular object of a given class. The value of an instance attribute is attached to the object itself. So, the attribute’s value is specific to its containing instance.
Python lets you dynamically attach attributes to existing objects that you’ve already created. However, you most often define instance attributes insideinstance methods, which are those methods that receiveself
as their first argument.
Note: Even though you can define instance attributes inside any instance method, it’s best to define all of them in the.__init__()
method, which is the instance initializer. This ensures that all of the attributes have consistent values when you create a new instance. Additionally, it makes the code more organized and easier to debug.
Consider the followingCar
class, which defines a bunch of instance attributes:
car.py
classCar:def__init__(self,make,model,year,color):self.make=makeself.model=modelself.year=yearself.color=colorself.started=Falseself.speed=0self.max_speed=200
In this class, you define a total of seven instance attributes inside.__init__()
. The attributes.make
,.model
,.year
, and.color
take values from the arguments to.__init__()
, which are the arguments that you must pass to the class constructor,Car()
, to create concrete objects.
Then, you explicitly initialize the attributes.started
,.speed
, and.max_speed
with sensible values that don’t come from the user.
Note: Inside a class, you must access all instance attributes through theself
argument. This argument holds a reference to thecurrent instance, which is where the attributes belong and live. Theself
argument plays a fundamental role in Python classes. You’ll learn more aboutself
in the sectionInstance Methods Withself
.
Here’s how yourCar
class works in practice:
>>>fromcarimportCar>>>toyota_camry=Car("Toyota","Camry",2022,"Red")>>>toyota_camry.make'Toyota'>>>toyota_camry.model'Camry'>>>toyota_camry.color'Red'>>>toyota_camry.speed0>>>ford_mustang=Car("Ford","Mustang",2022,"Black")>>>ford_mustang.make'Ford'>>>ford_mustang.model'Mustang'>>>ford_mustang.year2022>>>ford_mustang.max_speed200
In these examples, you create two different instances ofCar
. Each instance takes specific input arguments at instantiation time to initialize part of its attributes. Note how the values of the associated attributes are different and specific to the concrete instance.
Unlike class attributes, you can’t access instance attributes through the class. You need to access them through their containing instance:
>>>Car.makeTraceback (most recent call last):...AttributeError:type object 'Car' has no attribute 'make'
Instance attributes are specific to a concrete instance of a given class. So, you can’t access them through the class object. If you try to do that, then you get anAttributeError
exception.
.__dict__
AttributeIn Python, both classes and instances have a special attribute called.__dict__
. This attribute holds adictionary containing the writable members of the underlying class or instance. Remember, these members can be attributes or methods. Each key in.__dict__
represents an attribute name. The value associated with a given key represents the value of the corresponding attribute.
In a class,.__dict__
will contain class attributes and methods. In an instance,.__dict__
will hold instance attributes.
When you access a class member through the class object, Python automatically searches for the member’s name in the class.__dict__
. If the name isn’t there, then you get anAttributeError
.
Similarly, when you access an instance member through a concrete instance of a class, Python looks for the member’s name in the instance.__dict__
. If the name doesn’t appear there, then Python looks in the class.__dict__
. If the name isn’t found, then you get aNameError
.
Here’s a toy class that illustrates how this mechanism works:
sample_dict.py
classSampleClass:class_attr=100def__init__(self,instance_attr):self.instance_attr=instance_attrdefmethod(self):print(f"Class attribute:{self.class_attr}")print(f"Instance attribute:{self.instance_attr}")
In this class, you define a class attribute with a value of100
. In the.__init__()
method, you define an instance attribute that takes its value from the user’s input. Finally, you define a method to print both attributes.
Now it’s time to check the content of.__dict__
in the class object. Go ahead and run the following code:
>>>fromsample_dictimportSampleClass>>>SampleClass.class_attr100>>>SampleClass.__dict__mappingproxy({ '__module__': '__main__', 'class_attr': 100, '__init__': <function SampleClass.__init__ at 0x1036c62a0>, 'method': <function SampleClass.method at 0x1036c56c0>, '__dict__': <attribute '__dict__' of 'SampleClass' objects>, '__weakref__': <attribute '__weakref__' of 'SampleClass' objects>, '__doc__': None})>>>SampleClass.__dict__["class_attr"]100
The highlighted lines show that both the class attribute and the method are in the class.__dict__
dictionary. Note how you can use.__dict__
to access the value of class attributes by specifying the attribute’s name in square brackets, as you usually access keys in adictionary.
Note: You can access the same dictionary by calling the built-invars()
function on your class or instance, as you did before.
In instances, the.__dict__
dictionary will contain instance attributes only:
>>>instance=SampleClass("Hello!")>>>instance.instance_attr'Hello!'>>>instance.method()Class attribute: 100Instance attribute: Hello!>>>instance.__dict__{'instance_attr': 'Hello!'}>>>instance.__dict__["instance_attr"]'Hello!'>>>instance.__dict__["instance_attr"]="Hello, Pythonista!">>>instance.instance_attr'Hello, Pythonista!'
The instance.__dict__
dictionary in this example holds.instance_attr
and its specific value for the object at hand. Again, you can access any existing instance attribute using.__dict__
and the attribute name in square brackets.
You can modify the instance.__dict__
dynamically. This means that you can change the value of existing instance attributes through.__dict__
, as you did in the final example above. You can even add new attributes to an instance using its.__dict__
dictionary.
Using.__dict__
to change the value of instance attributes will allow you to avoidRecursionError
exceptions when you’re wiringdescriptors in Python. You’ll learn more about descriptors in theProperty and Descriptor-Based Attributes section.
In Python, you can add new attributes to your classes and instances dynamically. This possibility allows you to attach new data and behavior to your classes and objects in response to changing requirements or contexts. It also allows you to adapt existing classes to specific and dynamic needs.
For example, you can take advantage of this Python feature when you don’t know the required attributes of a given class at the time when you’re defining that class itself.
Consider the following class, which aims to store a row of data from a database table or aCSV file:
>>>classRecord:..."""Hold a record of data."""...
In this class, you haven’t defined any attributes or methods because you don’t know what data the class will store. Fortunately, you can add attributes and even methods to this class dynamically.
For example, say that you’ve read a row of data from anemployees.csv
file usingcsv.DictReader
. This class reads the data and returns it in a dictionary-like object. Now suppose that you have the following dictionary of data:
>>>john={..."name":"John Doe",..."position":"Python Developer",..."department":"Engineering",..."salary":80000,..."hire_date":"2020-01-01",..."is_manager":False,...}
Next, you want to add this data to an instance of yourRecord
class, and you need to represent each data field as an instance attribute. Here’s how you can do it:
>>>john_record=Record()>>>forfield,valueinjohn.items():...setattr(john_record,field,value)...>>>john_record.name'John Doe'>>>john_record.department'Engineering'>>>john_record.__dict__{ 'name': 'John Doe', 'position': 'Python Developer', 'department': 'Engineering', 'salary': 80000, 'hire_date': '2020-01-01', 'is_manager': False}
In this code snippet, you first create an instance ofRecord
calledjohn_record
. Then you run afor
loop toiterate over the items of your dictionary of data,john
. Inside the loop, you use the built-insetattr()
function to sequentially add each field as an attribute to yourjohn_record
object. If you inspectjohn_record
, then you’ll note that it stores all the original data as attributes.
You can also use dot notation and an assignment to add new attributes and methods to a class dynamically:
>>>classUser:...pass...>>># Add instance attributes dynamically>>>jane=User()>>>jane.name="Jane Doe">>>jane.job="Data Engineer">>>jane.__dict__{'name': 'Jane Doe', 'job': 'Data Engineer'}>>># Add methods dynamically>>>def__init__(self,name,job):...self.name=name...self.job=job...>>>User.__init__=__init__>>>User.__dict__mappingproxy({ ... '__init__': <function __init__ at 0x1036ccae0>})>>>linda=User("Linda Smith","Team Lead")>>>linda.__dict__{'name': 'Linda Smith', 'job': 'Team Lead'}
Here, you first create a minimalUser
class with no custom attributes or methods. To define the class’s body, you’ve just used apass
statement as a placeholder, which is Python’s way of doing nothing.
Then you create an object calledjane
. Note how you can use dot notation and an assignment to add new attributes to the instance. In this example, you add.name
and.job
attributes with appropriate values.
Then you provide theUser
class with an initializer or.__init__()
method. In this method, you take thename
andjob
arguments, which you turn into instance attributes in the method’s body. Then you add the method toUser
dynamically. After this addition, you can createUser
objects by passing the name and job to the class constructor.
As you can conclude from the above example, you can construct an entire Python class dynamically. Even though this capability of Python may seem neat, you must use it carefully because it can make your code difficult to understand and reason about.
Python allows you to add function-like behavior on top of existing instance attributes and turn them intomanaged attributes. This type of attribute prevents you from introducing breaking changes into your APIs.
In other words, with managed attributes, you can have function-like behavior and attribute-like access at the same time. You don’t need to change your APIs by replacing attributes with method calls, which can potentially break your users’ code.
To create a managed attribute with function-like behavior in Python, you can use either a property or a descriptor, depending on your specific needs.
Note: To dive deeper into Python properties, check outPython’sproperty()
: Add Managed Attributes to Your Classes.
As an example, get back to yourCircle
class and say that you need to validate the radius to ensure that it only stores positive numbers. How would you do that without changing your class interface? The quickest approach to this problem is to use a property and implement the validation logic in thesetter method.
Here’s what your new version ofCircle
can look like:
circle.py
importmathclassCircle:def__init__(self,radius):self.radius=radius@propertydefradius(self):returnself._radius@radius.setterdefradius(self,value):ifnotisinstance(value,int|float)orvalue<=0:raiseValueError("positive number expected")self._radius=valuedefcalculate_area(self):returnmath.pi*self._radius**2
To turn an existing attribute like.radius
into a property, you typically use the@property
decorator to write the getter method. The getter method must return the value of the attribute. In this example, the getter returns the circle’s radius, which is stored in the non-public._radius
attribute.
Note: The pipe sign (|
) in the call toisinstance()
expressesunion types. You can use this syntax in Python3.10 or greater. If you’re using a lower version of Python, then you can use a tuple of types(int, float)
.
To define the setter method of a property-based attribute, you need to use thedecorator@attr_name.setter
. In the example, you use@radius.setter
. Then you need to define the method itself. Note that property setters need to take an argument providing the value that you want to store in the underlying attribute.
Inside the setter method, you use a conditional to check whether the input value is an integer or a floating-point number. You also check if the value is less than or equal to0
. If either is true, then you raise aValueError
with a descriptive message about the actual issue. Finally, you assignvalue
to._radius,
and that’s it. Now,.radius
is a property-based attribute.
Here’s an example of this new version ofCircle
in action:
>>>fromcircleimportCircle>>>circle_1=Circle(100)>>>circle_1.radius100>>>circle_1.radius=500>>>circle_1.radius=0Traceback (most recent call last):...ValueError:positive number expected>>>circle_2=Circle(-100)Traceback (most recent call last):...ValueError:positive number expected>>>circle_3=Circle("300")Traceback (most recent call last):...ValueError:positive number expected
The first instance ofCircle
in this example takes a valid value for its radius. Note how you can continue working with.radius
as a regular attribute rather than as a method. If you try to assign an invalid value to.radius
, then you get aValueError
exception.
Note: Remember toreload thecircle.py
module if you’re working on the same REPL session as before. This recommendation will also be valid for all the examples in this tutorial where you change modules that you defined in previous examples.
It’s important to note that the validation also runs at instantiation time when you call the class constructor to create new instances ofCircle
. This behavior is consistent with your validation strategy.
Using a descriptor to create managed attributes is another powerful way to add function-like behavior to your instance attributes without changing your APIs. Like properties, descriptors can also have getter, setter, and other types of methods.
Note: To learn more about descriptors and how to use them, check outPython Descriptors: An Introduction.
To explore how descriptors work, say that you’ve decided to continue creating classes for your drawing application, and now you have the followingSquare
class:
square.py
classSquare:def__init__(self,side):self.side=side@propertydefside(self):returnself._side@side.setterdefside(self,value):ifnotisinstance(value,int|float)orvalue<=0:raiseValueError("positive number expected")self._side=valuedefcalculate_area(self):returnself._side**2
This class uses the same pattern as yourCircle
class. Instead of usingradius
, theSquare
class takes aside
argument and computes the area using the appropriate expression for a square.
This class is pretty similar toCircle
, and the repetition starts looking odd. Then you think of using a descriptor to abstract away the validation process. Here’s what you come up with:
shapes.py
importmathclassPositiveNumber:def__set_name__(self,owner,name):self._name=namedef__get__(self,instance,owner):returninstance.__dict__[self._name]def__set__(self,instance,value):ifnotisinstance(value,int|float)orvalue<=0:raiseValueError("positive number expected")instance.__dict__[self._name]=valueclassCircle:radius=PositiveNumber()def__init__(self,radius):self.radius=radiusdefcalculate_area(self):returnmath.pi*self.radius**2classSquare:side=PositiveNumber()def__init__(self,side):self.side=sidedefcalculate_area(self):returnself.side**2
The first thing to notice in this example is that you moved all the classes to ashapes.py
file. In that file, you define a descriptor class calledPositiveNumber
by implementing the.__get__()
and.__set__()
special methods, which are part of thedescriptor protocol.
Next, you remove the.radius
property fromCircle
and the.side
property fromSquare
. InCircle
, you add a.radius
class attribute, which holds an instance ofPositiveNumber
. You do something similar inSquare
, but the class attribute is appropriately named.side
.
Here are a few examples of how your classes work now:
>>>fromshapesimportCircle,Square>>>circle=Circle(100)>>>circle.radius100>>>circle.radius=500>>>circle.radius500>>>circle.radius=0Traceback (most recent call last):...ValueError:positive number expected>>>square=Square(200)>>>square.side200>>>square.side=300>>>square.side300>>>square=Square(-100)Traceback (most recent call last):...ValueError:positive number expected
Python descriptors provide a powerful tool for adding function-like behavior on top of your instance attributes. They can help you remove repetition from your code, making it cleaner and more maintainable. They also promote code reuse.
.__slots__
In a Python class, using the.__slots__
attribute can help you reduce the memory footprint of the corresponding instances. This attribute prevents the automatic creation of an instance.__dict__
. Using.__slots__
is particularly handy when you have a class with a fixed set of attributes, and you’ll use that class to create a large number of objects.
In the example below, you have aPoint
class with a.__slots__
attribute that consists of a tuple of allowed attributes. Each attribute will represent aCartesian coordinate:
>>>classPoint:...__slots__=("x","y")...def__init__(self,x,y):...self.x=x...self.y=y...>>>point=Point(4,8)>>>point.__dict__Traceback (most recent call last):...AttributeError:'Point' object has no attribute '__dict__'
ThisPoint
class defines.__slots__
as a tuple with two items. Each item represents the name of an instance attribute. So, they must be strings holding valid Pythonidentifiers.
Note: Although.__slots__
can hold alist
object, you should use atuple
object instead. Even if changing the list in.__slots__
after processing the class body had no effect, it’d be misleading to use amutable sequence there.
Instances of yourPoint
class don’t have a.__dict__
, as the code shows. This feature makes them memory-efficient. To illustrate this efficiency, you can measure the memory consumption of an instance ofPoint
. To do this, you can use thePympler library, which you can install fromPyPI using thepython -m pip install pympler
command.
Once you’ve installed Pympler withpip
, then you can run the following code in your REPL:
>>>frompymplerimportasizeof>>>asizeof.asizeof(Point(4,8))112
Theasizeof()
function from Pympler says that the objectPoint(4, 8)
occupies 112 bytes in your computer’s memory. Now get back to your REPL session and redefinePoint
without providing a.__slots__
attribute. With this update in place, go ahead and run the memory check again:
>>>classPoint:...def__init__(self,x,y):...self.x=x...self.y=y>>>asizeof.asizeof(Point(4,8))528
The same object,Point(4, 8)
, now consumes 528 bytes of memory. This number is over four times greater than what you got with the original implementation ofPoint
. Imagine how much memory.__slots__
would save if you had to create a million points in your code.
The.__slots__
attribute adds a second interesting behavior to your custom classes. It prevents you from adding new instance attributes dynamically:
>>>classPoint:...__slots__=("x","y")...def__init__(self,x,y):...self.x=x...self.y=y...>>>point=Point(4,8)>>>point.z=16Traceback (most recent call last):...AttributeError:'Point' object has no attribute 'z'
Adding a.__slots__
to your classes allows you to provide a series ofallowed attributes. This means that you won’t be able to add new attributes to your instances dynamically. If you try to do it, then you’ll get anAttributeError
exception.
A word of caution is in order, as many of Python’s built-in mechanisms implicitly assume that objects have the.__dict__
attribute. When you use.__slots__()
, then you waive that assumption, which means that some of those mechanisms might not work as expected anymore.
Python classes allow you to bundle data and behavior together in a single entity through attributes and methods, respectively. You’ll use the data to define the object’s current state and the methods to operate on that data or state.
A method is just a function that you define inside a class. By defining it there, you make the relationship between the class and the method explicit and clear.
Because they’re just functions, methods can take arguments andreturn values as functions do. However, the syntax for calling a method is a bit different from the syntax for calling a function. To call a method, you need to specify the class or instances in which that method is defined. To do this, you need to use dot notation. Remember that classes are namespaces, and their members aren’t directly accessible from the outside.
In a Python class, you can define three different types of methods:
self
, as their first argumentcls
, as their first argumentEvery type of method has its own characteristics and specific use cases. Instance methods are, by far, the most common methods that you’ll use in your custom Python classes.
Note: To learn more about instance, class, and static methods, check outPython’s Instance, Class, and Static Methods Demystified.
In the following sections, you’ll dive into how each of these methods works and how to create them in your classes. To get started, you’ll begin with instance methods, which are the most common methods that you’ll define in your classes.
self
In a class, an instance method is a function that takes the current instance as its first argument. In Python, this first argument is calledself
by convention.
Note: Naming the current instanceself
is a strong convention in Python. It’s so strong that it may look likeself
is one of the Python keywords. However, you could use any other name instead ofself
.
Even though it’s possible to use any name for the first argument of an instance method, usingself
is definitely the right choice because it’ll make your code look like Python code in the eyes of other developers.
Theself
argument holds a reference to the current instance, allowing you to access that instance from within methods. More importantly, throughself
, you can access and modify the instance attributes and call other methods within the class.
To define an instance method, you just need to write a regular function that acceptsself
as its first argument. Up to this point, you’ve already written some instance methods. To continue learning about them, turn back to yourCar
class.
Now say that you want to add methods to start, stop, accelerate, and brake the car. To kick things off, you’ll begin by writing the.start()
and.stop()
methods:
car.py
classCar:def__init__(self,make,model,year,color):self.make=makeself.model=modelself.year=yearself.color=colorself.started=Falseself.speed=0self.max_speed=200defstart(self):print("Starting the car...")self.started=Truedefstop(self):print("Stopping the car...")self.started=False
The.start()
and.stop()
methods are pretty straightforward. They take the current instance,self
, as their first argument. Inside the methods, you useself
to access the.started
attribute on the current instance using dot notation. Then you change the current value of this attribute toTrue
in.start()
and toFalse
in.stop()
. Both methods print informative messages to illustrate what your car is doing.
Note: Instance methods should act on instance attributes by either accessing them or changing their values. If you find yourself writing an instance method that doesn’t useself
in its body, then that may not be an instance method. In this case, you should probably use aclass method or astatic method, depending on your specific needs.
Now you can add the.accelerate()
and.brake()
methods, which will be a bit more complex:
car.py
classCar:def__init__(self,make,model,year,color):self.make=makeself.model=modelself.year=yearself.color=colorself.started=Falseself.speed=0self.max_speed=200# ...defaccelerate(self,value):ifnotself.started:print("Car is not started!")returnifself.speed+value<=self.max_speed:self.speed+=valueelse:self.speed=self.max_speedprint(f"Accelerating to{self.speed} km/h...")defbrake(self,value):ifself.speed-value>=0:self.speed-=valueelse:self.speed=0print(f"Braking to{self.speed} km/h...")
The.accelerate()
method takes an argument that represents the increment of speed that occurs when you call the method. For simplicity, you haven’t set any validation for the input increment of speed, so using negative values can cause issues. Inside the method, you first check whether the car’s engine is started, returning immediately if it’s not.
Then you check if the incremented speed is less than or equal to the allowed maximum speed for your car. If this condition is true, then you increment the speed. Otherwise, you set the speed to the allowed limit.
The.brake()
method works similarly. This time, you compare the decremented speed to0
because cars can’t have a negative speed. If the condition is true, then you decrement the speed according to the input argument. Otherwise, you set the speed to its lower limit,0
. Again, you have no validation on the input decrement of speed, so be careful with negative values.
Your class now has four methods that operate on and with its attributes. It’s time for a drive:
>>>fromcarimportCar>>>ford_mustang=Car("Ford","Mustang",2022,"Black")>>>ford_mustang.start()Starting the car...>>>ford_mustang.accelerate(100)Accelerating to 100 km/h...>>>ford_mustang.brake(50)Braking to 50 km/h...>>>ford_mustang.brake(80)Braking to 0 km/h...>>>ford_mustang.stop()Stopping the car...>>>ford_mustang.accelerate(100)Car is not started!
Great! YourCar
class works nicely! You can start your car’s engine, increment the speed, brake, and stop the car’s engine. You’re also confident that no one can increment the car’s speed if the car’s engine is stopped. How does that sound for minimal modeling of a car?
It’s important to note that when you call an instance method on a concrete instance likeford_mustang
using dot notation, you don’t have to provide a value for theself
argument. Python takes care of that step for you. It automatically passes the target instance toself
. So, you only have to provide the rest of the arguments.
However, you can manually provide the desired instance if you want. To do this, you need to call the method on the class:
>>>ford_mustang=Car("Ford","Mustang",2022,"Black")>>>Car.start(ford_mustang)Starting the car...>>>Car.accelerate(ford_mustang,100)Accelerating to 100 km/h...>>>Car.brake(ford_mustang,100)Braking to 0 km/h...>>>Car.stop(ford_mustang)Stopping the car...>>>Car.start()Traceback (most recent call last):...TypeError:Car.start() missing 1 required positional argument: 'self'
In this example, you call instance methods directly on the class. For this type of call to work, you need to explicitly provide an appropriate value to theself
argument. In this example, that value is theford_mustang
instance. Note that if you don’t provide a suitable instance, then the call fails with aTypeError
. The error message is pretty clear. There’s a missing positional argument,self
.
Python supports what it callsspecial methods, which are also known asdunder ormagic methods. These methods are typically instance methods, and they’re a fundamental part of Python’s internal class mechanism. They have an important feature in common:Python calls them automatically in response to specific operations.
Python uses these methods for many different tasks. They provide a great set of tools that will allow you to unlock the power of classes in Python.
You’ll recognize special methods because their names start and end with a double underscore, which is the origin of their other name,dunder methods (doubleunderscore). Arguably,.__init__()
is the most common special method in Python classes. As you already know, this method works as the instance initializer. Python automatically calls it when you call a class constructor.
Note: To dive deeper into special methods, check out thePython’s Magic Methods: Leverage Their Power in Your Classes tutorial.
You’ve already written a couple of.__init__()
methods. So, you’re ready to learn about other common and useful special methods. For example, the.__str__()
and.__repr__()
methods providestring representations for your objects.
Go ahead and update yourCar
class to add these two methods:
car.py
classCar:# ...def__str__(self):returnf"{self.make}{self.model},{self.color} ({self.year})"def__repr__(self):return(f"{type(self).__name__}"f'(make="{self.make}", 'f'model="{self.model}", 'f"year={self.year}, "f'color="{self.color}")')
The.__str__()
method provides what’s known as theinformal string representation of an object. This method must return a string that represents the object in a user-friendly manner. You can access an object’s informal string representation using eitherstr()
orprint()
.
The.__repr__()
method is similar, but it must return a string that allows you to re-create the object if possible. So, this method returns what’s known as theformal string representation of an object. This string representation is mostly targeted at Python programmers, and it’s pretty useful when you’re working in an interactive REPL session.
In interactive mode, Python falls back to calling.__repr__()
when you access an object or evaluate an expression, issuing the formal string representation of the resulting object. Inscript mode, you can access an object’s formal string representation using the built-inrepr()
function.
Run the following code to give your new methods a try. Remember that you need to restart your REPL or reloadcar.py
:
>>>fromcarimportCar>>>toyota_camry=Car("Toyota","Camry",2022,"Red")>>>str(toyota_camry)'Toyota Camry, Red (2022)'>>>print(toyota_camry)Toyota Camry, Red (2022)>>>toyota_camryCar(make="Toyota", model="Camry", year=2022, color="Red")>>>repr(toyota_camry)'Car(make="Toyota", model="Camry", year=2022, color="Red")'
When you use an instance ofCar
as an argument tostr()
orprint()
, you get a user-friendly string representation of the car at hand. This informal representation comes in handy when you need your programs to present your users with information about specific objects.
If you access an instance ofCar
directly in a REPL session, then you get a formal string representation of the object. You can copy and paste this representation to re-create the object in an appropriate environment. That’s why this string representation is intended to be useful for developers, who can take advantage of it while debugging and testing their code.
Pythonprotocols are another fundamental topic that’s closely related to special methods. Protocols consist of one or more special methods that support a given feature or functionality. Common examples of protocols include:
Protocol | Provided Feature | Special Methods |
---|---|---|
Iterator | Allows you to create iterator objects | .__iter__() and.__next__() |
Iterable | Makes your objects iterable | .__iter__() |
Descriptor | Lets you write managed attributes | .__get__() and optionally.__set__() ,.__delete__() , and.__set_name__() |
Context manager | Enables an object to work onwith statements | .__enter__() and.__exit__() |
Of course, Python has many other protocols that support cool features of the language. You already coded an example of using the descriptor protocol in theProperty and Descriptor-Based Attributes section.
Here’s an example of a minimalThreeDPoint
class that implements the iterable protocol:
>>>classThreeDPoint:...def__init__(self,x,y,z):...self.x=x...self.y=y...self.z=z...def__iter__(self):...yield from(self.x,self.y,self.z)...>>>list(ThreeDPoint(4,8,16))[4, 8, 16]
This class takes three arguments representing the space coordinates of a given point. The.__iter__()
method is agenerator function that returns an iterator. The resulting iteratoryields the coordinates ofThreeDPoint
on demand.
The call tolist()
iterates over the attributes.x
,.y
, and.z
, returning alist
object. You don’t need to call.__iter__()
directly. Python calls it automatically when you use an instance ofThreeDPoint
in aniteration.
@classmethod
You can also addclass methods to your custom Python classes. A class method is a method that takes the class object as its first argument instead of takingself
. In this case, the argument should be calledcls
, which is also a strong convention in Python. So, you should stick to it.
You can create class methods using the@classmethod
decorator. Providing your classes withmultiple constructors is one of the most common use cases of class methods in Python.
For example, say you want to add an alternative constructor to yourThreeDPoint
so that you can quickly create points from tuples or lists of coordinates:
point.py
classThreeDPoint:def__init__(self,x,y,z):self.x=xself.y=yself.z=zdef__iter__(self):yield from(self.x,self.y,self.z)@classmethoddeffrom_sequence(cls,sequence):returncls(*sequence)def__repr__(self):returnf"{type(self).__name__}({self.x},{self.y},{self.z})"
In the.from_sequence()
class method, you take a sequence of coordinates as an argument, create aThreeDPoint
object from it, and return the object to the caller. To create the new object, you use thecls
argument, which holds an implicit reference to the current class, which Python injects into your method automatically.
Here’s how this class method works:
>>>frompointimportThreeDPoint>>>ThreeDPoint.from_sequence((4,8,16))ThreeDPoint(4, 8, 16)>>>point=ThreeDPoint(7,14,21)>>>point.from_sequence((3,6,9))ThreeDPoint(3, 6, 9)
In this example, you use theThreeDPoint
class directly to access the class method.from_sequence()
. Note that you can also access the method using a concrete instance, likepoint
in the example. In each of the calls to.from_sequence()
, you’ll get a completely new instance ofThreeDPoint
. However, class methods should be accessed through the corresponding class name for better clarity and to avoid confusion.
@staticmethod
Your Python classes can also havestatic methods. These methods don’t take the instance or the class as an argument. So, they’re regular functions defined within a class. You could’ve also defined them outside the class as stand-alone function.
You’ll typically define a static method instead of a regular function outside the class when that function is closely related to your class, and you want to bundle it together for convenience or for consistency with your code’s API. Remember that calling a function is a bit different from calling a method. To call a method, you need to specify a class or object that provides that method.
If you want to write a static method in one of your custom classes, then you need to use the@staticmethod
decorator. Check out the.show_intro_message()
method below:
point.py
classThreeDPoint:def__init__(self,x,y,z):self.x=xself.y=yself.z=zdef__iter__(self):yield from(self.x,self.y,self.z)@classmethoddeffrom_sequence(cls,sequence):returncls(*sequence)@staticmethoddefshow_intro_message(name):print(f"Hey{name}! This is your 3D Point!")def__repr__(self):returnf"{type(self).__name__}({self.x},{self.y},{self.z})"
The.show_intro_message()
static method takes aname
as an argument and prints a message on the screen. Note that this is only a toy example of how to write static methods in your classes.
Static methods like.show_intro_message()
don’t operate on the current instance,self
, or the current class,cls
. They work as independent functions enclosed in a class. You’ll typically put them inside a class when they’re closely related to that class but don’t necessarily affect the class or its instances.
Here’s how the method works:
>>>frompointimportThreeDPoint>>>ThreeDPoint.show_intro_message("Pythonista")Hey Pythonista! This is your 3D Point!>>>point=ThreeDPoint(2,4,6)>>>point.show_intro_message("Python developer")Hey Python developer! This is your 3D Point!
As you already know, the.show_intro_message()
method takes a name as an argument and prints a message to your screen. Note that you can call the method using the class or any of its instances. As with class methods, you should generally call static methods through the corresponding class instead of one of its instances.
Programming languages likeJava andC++ don’t expose attributes as part of their classes’ public APIs. Instead, these programming languages make extensive use of getter and setter methods to give you access to attributes.
Note: To dive deeper into the getter and setter pattern and how Python approaches it, check outGetters and Setters: Manage Attributes in Python.
Using methods to access and update attributes promotesencapsulation. Encapsulation is a fundamental OOP principle that recommends protecting an object’s state or data from the outside world, preventing direct access. The object’s state should only be accessible through a public interface consisting of getter and setter methods.
For example, say that you have aPerson
class with a.name
instance attribute. You can make.name
a non-public attribute and provide getter and setter methods to access and change that attribute:
person.py
classPerson:def__init__(self,name):self.set_name(name)defget_name(self):returnself._namedefset_name(self,value):self._name=value
In this example,.get_name()
is the getter method and allows you to access the underlying._name
attribute. Similarly,.set_name()
is the setter method and allows you to change the current value of._name
. The._name
attribute is non-public and is where the actual data is stored.
Here’s how you can use yourPerson
class:
>>>frompersonimportPerson>>>jane=Person("Jane")>>>jane.get_name()'Jane'>>>jane.set_name("Jane Doe")>>>jane.get_name()'Jane Doe'
Here, you create an instance ofPerson
using the class constructor and"Jane"
as the required name. That means you can use the.get_name()
method to access Jane’s name and the.set_name()
method to update it.
The getter and setter pattern is common in languages like Java and C++. Besides promoting encapsulation and APIs centered on method calls, this pattern also allows you to quickly add function-like behavior to your attributes without introducing breaking changes in your APIs.
However, this pattern is less popular in the Python community. In Python, it’s completely normal to expose attributes as part of an object’s public API. If you ever need to add function-like behavior on top of a public attribute, then you can turn it into a property instead of breaking the API by replacing the attribute with a method.
Here’s how most Python developers would write thePerson
class:
person.py
classPerson:def__init__(self,name):self.name=name
This class doesn’t have getter and setter methods for the.name
attribute. Instead, it exposes the attribute as part of its API. So, you can use it directly:
>>>frompersonimportPerson>>>jane=Person("Jane")>>>jane.name'Jane'>>>jane.name="Jane Doe">>>jane.name'Jane Doe'
In this example, instead of using a setter method to change the value of.name
, you use the attribute directly in an assignment statement. This is common practice in Python code. If yourPerson
class evolves to a point where you need to add function-like behavior on top of.name
, then you can turn the attribute into a property.
For example, say that you need to store the attribute in uppercase letters. Then you can do something like the following:
person.py
classPerson:def__init__(self,name):self.name=name@propertydefname(self):returnself._name@name.setterdefname(self,value):self._name=value.upper()
This class defines.name
as a property with appropriate getter and setter methods. Python will automatically call these methods, respectively, when you access or update the attribute’s value. The setter method takes care of uppercasing the input value before assigning it back to._name
:
>>>frompersonimportPerson>>>jane=Person("Jane")>>>jane.name'JANE'>>>jane.name="Jane Doe">>>jane.name'JANE DOE'
Python properties allow you to add function-like behavior to your attributes while you continue to use them as normal attributes instead of as methods. Note how you can still assign new values to.name
using an assignment instead of a method call. Running the assignment triggers the setter method, which uppercases the input value.
Up to this point, you’ve learned a lot about Python classes: how to create them, when and how to use them in your code, and more. In this section, you’ll review that knowledge by writing a class that integrates most of the syntax and features you’ve learned so far.
Your class will represent an employee of a given company and will implement attributes and methods to manage some related tasks like keeping track of personal information and computing the employee’s age. To kick things off, go ahead and fire up your favoritecode editor or IDE and create a file calledemployee.py
. Then add the following code to it:
employee.py
classEmployee:company="Example, Inc."def__init__(self,name,birth_date):self.name=nameself.birth_date=birth_date
In thisEmployee
class, you define a class attribute called.company
. This attribute will hold the company’s name, which is common to all employees on the payroll.
Then you define the initializer,.__init__()
, which takes the employee’s name and birth date as arguments. Remember that you must pass appropriate values for both arguments when you call the class constructor,Employee()
.
Inside.__init__()
, you define two public instance attributes to store the employee’s name and birth date. These attributes will be part of the class API because they’re public attributes.
Now say that you want to turn.birth_date
into a property to automatically convert the input date inISO format to adatetime
object:
employee.py
fromdatetimeimportdatetimeclassEmployee:# ...@propertydefbirth_date(self):returnself._birth_date@birth_date.setterdefbirth_date(self,value):self._birth_date=datetime.fromisoformat(value)
Here, you define the.birth_date
property through the@property
decorator. The getter method returns the content of._birth_date
. This non-public attribute will hold the concrete data.
To define the setter method, you use the@birth_date.setter
decorator. In this method, you assign adatetime.datetime
object to._birth_date
. In this example, you don’t run any validation on the input data, which should be a string holding the date in ISO format. You can implement the validation as an exercise.
Next, say you want to write a regular instance method to compute the employee’s age from their birth date:
employee.py
fromdatetimeimportdatetimeclassEmployee:# ...defcompute_age(self):today=datetime.today()age=today.year-self.birth_date.yearbirthday=datetime(today.year,self.birth_date.month,self.birth_date.day)iftoday<birthday:age-=1returnage
Here,.compute_age()
is an instance method because it takes the current instance,self
, as its first argument. Inside the method, you compute the employee’s age using the.birth_date
property as a starting point.
Now say that you’ll often build instances ofEmployee
from dictionaries containing the data of your employees. You can add a convenient class method to quickly build objects that way:
employee.py
fromdatetimeimportdatetimeclassEmployee:# ...@classmethoddeffrom_dict(cls,data_dict):returncls(**data_dict)
In this code snippet, you define a class method using the@classmethod
decorator. The method takes a dictionary object containing the data of a given employee. Then it builds an instance ofEmployee
using thecls
argument andunpacks the dictionary.
Finally, you’ll add suitable.__str__()
and.__repr__()
special methods to make your class friendly to users and developers, respectively:
employee.py
fromdatetimeimportdatetimeclassEmployee:# ...def__str__(self):returnf"{self.name} is{self.compute_age()} years old"def__repr__(self):return(f"{type(self).__name__}("f"name='{self.name}', "f"birth_date='{self.birth_date.strftime('%Y-%m-%d')}')")
The.__str__()
method returns a string describing the current employee in a user-friendly manner. Similarly, the.__repr__()
method returns a string that will allow you to re-create the current object, which is great from a developer’s perspective.
Here’s how you can useEmployee
in your code:
>>>fromemployeeimportEmployee>>>john=Employee("John Doe","1998-12-04")>>>john.companyExample, Inc.>>>john.name'John Doe'>>>john.compute_age()25>>>print(john)John Doe is 25 years old>>>johnEmployee(name='John Doe', birth_date='1998-12-04')>>>jane_data={"name":"Jane Doe","birth_date":"2001-05-15"}>>>jane=Employee.from_dict(jane_data)>>>print(jane)Jane Doe is 23 years old
Cool! YourEmployee
class works great so far! It allows you to represent employees, access their attributes, and compute their ages. It also provides neat string representations that will make your class look polished and reliable. Great job! Do you have any ideas of cool features that you could add toEmployee
?
Debugging often represents a large portion of your coding time. You’ll probably spend long hours tracking errors in the code that you’re working on and trying to fix them to make the code more robust and reliable. When you start working with classes and objects in Python, you’re likely to encounter some new exceptions.
For example, if you try to access an attribute or method that doesn’t exist, then you’ll get anAttributeError
exception:
>>>classPoint:...def__init__(self,x,y):...self.x=x...self.y=y...>>>point=Point(4,8)>>>point.zTraceback (most recent call last):...AttributeError:'Point' object has no attribute 'z'
ThePoint
class doesn’t define a.z
instance attribute, so you get anAttributeError
exception if you try to access that attribute.
You’ll find a fewexceptions that can occur when working with Python classes. These are some of the most common ones:
AttributeError
occurs when the specified object doesn’t define the attribute or method that you’re trying to access. Take, for example, accessing.z
on thePoint
class defined in the above example.TypeError
occurs when you apply an operation or function to an object that doesn’t support that operation. For example, consider calling the built-inlen()
function with a number as an argument.NotImplementedError
occurs when an abstract method isn’t implemented in a concrete subclass. You’ll learn more about this exception in the sectionCreating Abstract Base Classes (ABC) and Interfaces.These are just a few examples of exceptions that can occur when you’re working with Python classes. You’ll also find some common mistakes that people sometimes make when they start to write their own classes:
self
argument in instance methodsThese are just a few common mistakes that people might make when they’re getting started with Python classes. From this list, you haven’t learned aboutinheritance yet. Don’t worry about it for now. Inheritance is an advanced topic that you’ll study later in this tutorial.
In the Pythonstandard library, you’ll find many tools that solve different problems and deal with different challenges. Among all these tools, you’ll find a few that will make you more productive when writing custom classes.
For example, if you want a tool that saves you from writing a lot of class-related boilerplate code, then you can take advantage of data classes and thedataclasses
module.
Similarly, if you’re looking for a tool that allows you to quickly create class-based enumerations ofconstants, then you can turn your eye to theenum
module and its different types of enumeration classes.
In the following sections, you’ll learn the basics of using data classes and enumerations to efficiently write robust, reliable, and specialized classes in Python.
Python’s data classes specialize in storing data. However, they’re alsocode generators that produce a lot of class-relatedboilerplate code for you behind the scenes.
For example, if you use the data class infrastructure to write a custom class, then you won’t have to implement special methods like.__init__()
,.__repr__()
,.__eq__()
, and.__hash__()
. The data class will write them for you. More importantly, the data class will write these methods applying best practices and avoiding potential errors.
Note: To learn more about data classes in Python, check out theData Classes in Python 3.7+ (Guide) tutorial.
As you already know, special methods support important functionalities in Python classes. In the case of data classes, you’ll have accurate string representation, comparison capabilities, hashability, and more.
Even though the namedata class may suggest that this type of class is limited to containing data, it can also contain methods. So, data classes are like regular classes but with superpowers.
To create a data class, go ahead and import the@dataclass
decorator from thedataclasses
module. You’ll use this decorator in the definition of your class. This time, you won’t write an.__init__()
method. You’ll just define data fields as class attributes withtype hints.
For example, here’s how you can write theThreeDPoint
class as a data class:
point.py
fromdataclassesimportdataclass@dataclassclassThreeDPoint:x:int|floaty:int|floatz:int|float@classmethoddeffrom_sequence(cls,sequence):returncls(*sequence)@staticmethoddefshow_intro_message(name):print(f"Hey{name}! This is your 3D Point!")
This new implementation ofThreeDPoint
uses Python’s@dataclass
decorator to turn the regular class into a data class. Instead of defining an.__init__()
method, you list the attributes with their corresponding types. The data class will take care of writing a proper initializer for you. Note that you don’t define.__iter__()
or.__repr__()
either.
Note: Data classes are pretty flexible when it comes to defining their fields or attributes. You can declare them with the type annotation syntax. You can initialize them with a sensible default value. You can also combine both approaches depending on your needs:
point.py
fromdataclassesimportdataclass@dataclassclassThreeDPoint:x:int|floaty=0.0z:int|float=0.0# ...
In this code snippet, you declare the first attribute using the type annotation syntax. The second attribute has a default value with no type annotation. Finally, the third attribute has both type annotation and a default value. However, when you don’t specify a type hint for an attribute, then Python won’t automatically generate the corresponding code for that attribute.
Once you’ve defined the data fields or attributes, you can start adding methods. In this example, you keep the.from_sequence()
class method and the.show_intro_message()
static method.
Go ahead and run the following code to check the additional functionality that@dataclass
has added to this version ofThreeDPoint
:
>>>fromdataclassesimportastuple>>>frompointimportThreeDPoint>>>point_1=ThreeDPoint(1.0,2.0,3.0)>>>point_1ThreeDPoint(x=1.0, y=2.0, z=3.0)>>>astuple(point_1)(1.0, 2.0, 3.0)>>>point_2=ThreeDPoint(2,3,4)>>>point_1==point_2False>>>point_3=ThreeDPoint(1,2,3)>>>point_1==point_3True
YourThreeDPoint
class works pretty well! It provides a suitable string representation with an automatically generated.__repr__()
method. You can iterate over the fields using theastuple()
function from thedataclasses
module. Finally, you can compare two instances of the class for equality (==
). As you can conclude, this new version ofThreeDPoint
has saved you from writing several lines of tricky boilerplate code.
Anenumeration, or justenum, is a data type that you’ll find in several programming languages. Enums allow you to create sets ofnamed constants, which are known asmembers and can be accessed through the enumeration itself.
Python doesn’t have a built-in enum data type. However, Python 3.4 introduced theenum
module to provide theEnum
class for supporting general-purpose enumerations.
Days of the week, months and seasons of the year, HTTP status codes, colors in a traffic light, and pricing plans of a web service are all great examples of constants that you can group in an enum. In short, you can use enums to represent variables that can take one of alimited set of possible values.
TheEnum
class, among other similar classes in theenum
module, allows you to quickly and efficiently create custom enumerations or groups of similar constants with neat features that you don’t have to code yourself. Apart from member constants, enums can also have methods to operate with those constants.
Note: To learn more about how to create and use enumerations in your Python code, check out theBuild Enumerations of Constants With Python’s Enum tutorial.
To define a custom enumeration, you can subclass theEnum
class. Here’s an example of an enumeration that groups the days of the week:
>>>fromenumimportEnum>>>classWeekDay(Enum):...MONDAY=1...TUESDAY=2...WEDNESDAY=3...THURSDAY=4...FRIDAY=5...SATURDAY=6...SUNDAY=7...
In this code example, you defineWeekDay
by subclassingEnum
from theenum
module. This specific enum groups seven constants representing the days of the week. These constants are the enum members. Because they’re constants, you should follow the convention for naming any constant in Python: uppercase letters and underscores between words, if applicable.
Enumerations have a few cool features that you can take advantage of. For example, their members are strict constants, so you can’t change their values. They’re also iterable by default:
>>>WeekDay.MONDAY=0Traceback (most recent call last):...AttributeError:cannot reassign member 'MONDAY'>>>list(WeekDay)[ <WeekDay.MONDAY: 1>, <WeekDay.TUESDAY: 2>, <WeekDay.WEDNESDAY: 3>, <WeekDay.THURSDAY: 4>, <WeekDay.FRIDAY: 5>, <WeekDay.SATURDAY: 6>, <WeekDay.SUNDAY: 7>]
If you try to change the value of an enum member, then you get anAttributeError
. So, enum members are strictly constants. You can iterate over the members directly because enumerations support iteration by default.
You can directly access their members using different syntax:
>>># Dot notation>>>WeekDay.MONDAY<WeekDay.MONDAY: 1>>>># Call notation>>>WeekDay(2)<WeekDay.TUESDAY: 2>>>># Dictionary notation>>>WeekDay["WEDNESDAY"]<WeekDay.WEDNESDAY: 3>
In the first example, you access an enum member usingdot notation, which is pretty intuitive and readable. In the second example, you access a member bycalling the enumeration with that member’s value as an argument. Finally, you use a dictionary-like syntax to access another member by name.
If you want fine-grain access to a member’s components, then you can use the.name
and.value
attributes:
>>>WeekDay.THURSDAY.name'THURSDAY'>>>WeekDay.THURSDAY.value4>>>fordayinWeekDay:...print(day.name,"->",day.value)...MONDAY -> 1TUESDAY -> 2WEDNESDAY -> 3THURSDAY -> 4FRIDAY -> 5SATURDAY -> 6SUNDAY -> 7
In these examples, you access the.name
and.value
attributes of specific members ofWeekDay
. These attributes provide access to each member’s component.
Finally, you can also add custom behavior to your enumerations. To do that, you can use methods as you’d do with regular classes:
week.py
fromenumimportEnumclassWeekDay(Enum):MONDAY=1TUESDAY=2WEDNESDAY=3THURSDAY=4FRIDAY=5SATURDAY=6SUNDAY=7@classmethoddeffavorite_day(cls):returncls.FRIDAYdef__str__(self):returnf"Current day:{self.name}"
After saving your code toweek.py
, you add a class method called.favorite_day()
to yourWeekDay
enumeration. This method will just return your favorite day of the week, which is Friday, of course! Then you add a.__str__()
method to provide a user-friendly string representation for the current day.
Here’s how you can use these methods in your code:
>>>fromweekimportWeekDay>>>WeekDay.favorite_day()<WeekDay.FRIDAY: 5>>>>print(WeekDay.FRIDAY)Current day: FRIDAY
You’ve added new functionality to your enumeration through class and instance methods. Isn’t that cool?
Inheritance is a powerful feature of object-oriented programming. It consists of creating hierarchical relationships between classes, where child classes inherit attributes and methods from their parent class. In Python, one class can have multiple parents or, more broadly, ancestors.
This is known asimplementation inheritance, which allows you to reduce duplication and repetition by code reuse. It can also make your code more modular, better organized, and more scalable. However, classes alsoinherit the interface by becoming more specialized kinds of their ancestors. In some cases, you’ll be able to use a child instance where an ancestor is expected.
In the following sections, you’ll learn how to use inheritance in Python. You’ll start with simple inheritance and continue with more complex concepts. So, get ready! This is going to be fun!
When you have a class that inherits from a single parent class, then you’re usingsingle-base inheritance or justsimple inheritance. To make a Python class inherit from another, you need to list the parent class’s name in parentheses after the child class’s name in the definition.
Here’s the syntax that you must use:
classParent:# Parent's definition goes here...<body>classChild(Parent):# Child definitions goes here...<body>
In this code snippet,Parent
is the class you want to inherit from. Parent classes typically provide generic and common functionality that you can reuse throughout multiple child classes.Child
is the class that inherits attributes and methods fromParent
. The highlighted line shows the required syntax.
Note: In this tutorial, you’ll use the termsparent class,superclass, andbase class interchangeably to refer to the class that you inherit from.
Similarly, you’ll use the termschild class,derived class, andsubclass to refer to classes that inherit from other classes.
Here’s a practical example to get started with simple inheritance and how it works. Suppose you’re building an app to track vehicles and routes. At first, the app will track cars and motorcycles. You think of creating aVehicle
class and deriving two subclasses from it. One subclass will represent a car, and the other will represent a motorcycle.
TheVehicle
class will provide common attributes, such as.make
,.model
, and.year
. It’ll also provide the.start()
and.stop()
methods to start and stop the vehicle engine, respectively:
vehicles.py
classVehicle:def__init__(self,make,model,year):self.make=makeself.model=modelself.year=yearself._started=Falsedefstart(self):print("Starting engine...")self._started=Truedefstop(self):print("Stopping engine...")self._started=False
In this code, you define theVehicle
class with attributes and methods that will be common to all your vehicle types. You can say thatVehicle
provides the common interface of all your vehicles. You’ll inherit from this class to reuse this interface and its functionality in your subclasses.
Now you can define theCar
andMotorcycle
classes. Both of them will have some unique attributes and methods specific to the vehicle type. For example, theCar
will have a.num_seats
attribute and a.drive()
method:
vehicles.py
# ...classCar(Vehicle):def__init__(self,make,model,year,num_seats):super().__init__(make,model,year)self.num_seats=num_seatsdefdrive(self):print(f'Driving my "{self.make} -{self.model}" on the road')def__str__(self):returnf'"{self.make} -{self.model}" has{self.num_seats} seats'
YourCar
class usesVehicle
as its parent class. This means thatCar
will automatically inherit the.make
,.model
, and.year
attributes, as well as the non-public._started
attribute. It’ll also inherit the.start()
and.stop()
methods.
Note: Like inheritance in biology, inheritance in OOP goes in a single direction, from the parents to the children. In other words, children inherit from their parents and not the other way around.
The class defines a.num_seats
attribute. As you already know, you should define and initialize instance attributes in.__init__()
. This requires you to provide a custom.__init__()
method inCar
, which will shadow the superclass initializer.
How can you write an.__init__()
method inCar
and still guarantee that you initialize the.make
,.model
, and.year
attributes? That’s where the built-insuper()
function comes on the scene. This function allows you to access members in the superclass, as its name suggests.
Note: To learn more about usingsuper()
in your classes, check outSupercharge Your Classes With Pythonsuper()
.
InCar
, you usesuper()
to call the.__init__()
method onVehicle
. Note that you pass the input values for.make
,.model
, and.year
so thatVehicle
can initialize these attributes correctly. After this call tosuper()
, you add and initialize the.num_seats
attribute, which is specific to theCar
class.
Finally, you write the.drive()
method, which is also specific toCar
. This method is just a demonstrative example, so it only prints a message to your screen.
Now it’s time to define theMotorcycle
class, which will inherit fromVehicle
too. This class will have a.num_wheels
attribute and a.ride()
method:
vehicles.py
# ...classMotorcycle(Vehicle):def__init__(self,make,model,year,num_wheels):super().__init__(make,model,year)self.num_wheels=num_wheelsdefride(self):print(f'Riding my "{self.make} -{self.model}" on the road')def__str__(self):returnf'"{self.make} -{self.model}" has{self.num_wheels} wheels'
Again, you callsuper()
to initialize.make
,.model
, and.year
. After that, you define and initialize the.num_wheels
attribute. Finally, you write the.ride()
method. Again, this method is just a demonstrative example.
With this code in place, you can start usingCar
andMotorcycle
right away:
>>>fromvehiclesimportCar,Motorcycle>>>tesla=Car("Tesla","Model S",2022,5)>>>tesla.start()Starting engine...>>>tesla.drive()Driving my "Tesla - Model S" on the road>>>tesla.stop()Stopping engine...>>>print(tesla)"Tesla - Model S" has 5 seats>>>harley=Motorcycle("Harley-Davidson","Iron 883",2021,2)>>>harley.start()Starting engine...>>>harley.ride()Riding my "Harley-Davidson - Iron 883" on the road.>>>harley.stop()Stopping engine...>>>print(harley)"Harley-Davidson - Iron 883" has 2 wheels
Cool! Your Tesla and your Harley-Davidson work nicely. You can start their engines, drive or ride them, and so on. Note how you can use both the inherited and specific attributes and methods in both classes.
You’ll typically use single inheritance or inheritance in general when you have classes that share common attributes and behaviors and want to reuse them in derived classes. So, inheritance is a great tool for code reuse. Subclasses will inherit and reuse functionality from their parent.
Subclasses will frequently extend their parent’s interface with new attributes and methods. You can use them as a new starting point to create another level of inheritance. This practice will lead to the creation of class hierarchies.
Using inheritance, you can design and buildclass hierarchies, also known asinheritance trees. A class hierarchy is a set of closely related classes that are connected through inheritance and arranged in a tree-like structure.
The class or classes at the top of the hierarchy are the base classes, while the classes below are derived classes or subclasses. Inheritance-based hierarchies express anis-a-type-of relationship between subclasses and their base classes. For example,a bird is a type of animal.
Each level in the hierarchy will inherit attributes and behaviors from the above levels. Therefore, classes at the top of the hierarchy are generic classes with common functionality, while classes down the hierarchy are more specialized. They’ll inherit attributes and behaviors from their superclasses and will also add their own.
Taxonomic classification of animals is a commonly used example to explain class hierarchies. In this hierarchy, you’ll have a genericAnimal
class at the top. Below this class, you can have subclasses likeMammal
,Bird
,Fish
, and so on. These subclasses are more specific classes thanAnimal
and inherit the attributes and methods from it. They can also have their own attributes and methods.
To continue with the hierarchy, you can subclassMammal
,Bird
, andFish
and create derived classes with even more specific characteristics. Here’s a short toy implementation of this example:
animals.py
classAnimal:def__init__(self,name,sex,habitat):self.name=nameself.sex=sexself.habitat=habitatclassMammal(Animal):unique_feature="Mammary glands"classBird(Animal):unique_feature="Feathers"classFish(Animal):unique_feature="Gills"classDog(Mammal):defwalk(self):print("The dog is walking")classCat(Mammal):defwalk(self):print("The cat is walking")classEagle(Bird):deffly(self):print("The eagle is flying")classPenguin(Bird):defswim(self):print("The penguin is swimming")classSalmon(Fish):defswim(self):print("The salmon is swimming")classShark(Fish):defswim(self):print("The shark is swimming")
At the top of the hierarchy, you have theAnimal
class. This is the base class of your hierarchy. It has the.name
,.sex
, and.habitat
attributes, which will be string objects. These attributes are common to all animals.
Then you define theMammal
,Bird
, andFish
classes by inheriting fromAnimal
. These classes have a.unique_feature
class attribute that holds the distinguishing characteristic of each group of animals.
Then you create concrete mammals likeDog
andCat
. These classes have specific methods that are common to all dogs and cats, respectively. Similarly, you define two classes that inherit fromBird
and two more that inherit fromFish
.
Here’s a tree-likeclass diagram that will help you see the hierarchical relationship between classes:
Each level in the hierarchy can—and typically will—add new attributes and functionality on top of those that its parents already provide. If you walk through the diagram from top to button, then you’ll move from generic to specialized classes.
These latter classes implement new methods that are specific to the class at hand. In this example, the methods just print some information to the screen and automaticallyreturnNone
, which is thenull value in Python.
Note: You can create class diagrams to represent class hierarchies that are based on inheritance. However, that’s not the only relationship that can appear between your classes.
With class diagrams, you can also represent other types of relationships, including the following:
You’ll learn more about some of these types of relationships in the section calledUsing Alternatives to Inheritance.
That’s how you design and create class hierarchies to reuse code and functionality. Such hierarchies also allow you to give your code a modular organization, making it more maintainable and scalable.
When you’re using inheritance, you can face an interesting and challenging issue. In some situations, a parent class may provide a given functionality only at a basic level, and you may want to extend that functionality in your subclasses. In other situations, the feature in the parent class isn’t appropriate for the subclass.
In these situations, you can use one of the following strategies, depending on your specific case:
Here’s an example of a small class hierarchy that applies the first strategy to provide extended functionality based on the inherited one:
aircrafts.py
classAircraft:def__init__(self,thrust,lift,max_speed):self.thrust=thrustself.lift=liftself.max_speed=max_speeddefshow_technical_specs(self):print(f"Thrust:{self.thrust} kW")print(f"Lift:{self.lift} kg")print(f"Max speed:{self.max_speed} km/h")classHelicopter(Aircraft):def__init__(self,thrust,lift,max_speed,num_rotors):super().__init__(thrust,lift,max_speed)self.num_rotors=num_rotorsdefshow_technical_specs(self):super().show_technical_specs()print(f"Number of rotors:{self.num_rotors}")
In this example, you defineAircraft
as the base class. In.__init__()
, you create a few instance attributes. Then you define the.show_technical_specs()
method, which prints information about the aircraft’s technical specifications.
Next, you defineHelicopter
, inheriting fromAircraft
. The.__init__()
method ofHelicopter
extends the corresponding method ofAircraft
by callingsuper()
to initialize the.thrust
,.lift
, and.max_speed
attributes. You already saw something like this in the previous section.
Helicopter
also extends the functionality of.show_technical_specs()
. In this case, you first call.show_technical_specs()
fromAircraft
usingsuper()
. Then you add a new call toprint()
that adds new information to the technical description of the helicopter at hand.
Here’s howHelicopter
instances work in practice:
>>>fromaircraftsimportHelicopter>>>sikorsky_UH60=Helicopter(1490,9979,278,2)>>>sikorsky_UH60.show_technical_specs()Thrust: 1490 kWLift: 9979 kgMax speed: 278 km/hNumber of rotors: 2
When you call.show_technical_specs()
on aHelicopter
instance, you get the information provided by the base class,Aircraft
, and also the specific information added byHelicopter
itself. You’ve extended the functionality ofAircraft
in its subclassHelicopter
.
Now it’s time to take a look at how you can override a method in a subclass. As an example, say that you have a base class calledWorker
that defines several attributes and methods like in the following example:
workers.py
classWorker:def__init__(self,name,address,hourly_salary):self.name=nameself.address=addressself.hourly_salary=hourly_salarydefshow_profile(self):print("== Worker profile ==")print(f"Name:{self.name}")print(f"Address:{self.address}")print(f"Hourly salary:{self.hourly_salary}")defcalculate_payroll(self,hours=40):returnself.hourly_salary*hours
In this class, you define a few instance attributes to store important data about the current worker. You also provide the.show_profile()
method to display relevant information about the worker. Finally, you write a generic.calculate_payroll()
method to compute the salary of workers from their hourly salary and the number of hours worked.
Later in the development cycle, some requirements change. Now you realize that managers compute their salaries in a different way. They’ll have an hourly bonus that you must add to the normal hourly salary before computing the final amount.
After thinking a bit about the problem, you decide thatManager
has to override.calculate_payroll()
completely. Here’s the implementation that you come up with:
workers.py
# ...classManager(Worker):def__init__(self,name,address,hourly_salary,hourly_bonus):super().__init__(name,address,hourly_salary)self.hourly_bonus=hourly_bonusdefcalculate_payroll(self,hours=40):return(self.hourly_salary+self.hourly_bonus)*hours
In theManager
initializer, you take the hourly bonus as an argument. Then you call the parent’s.__init__()
method as usual and define the.hourly_bonus
instance attribute. Finally, you override.calculate_payroll()
with a completely different implementation that doesn’t reuse the inherited functionality.
In Python, you can usemultiple inheritance. This type of inheritance allows you to create a class that inherits from several parent classes. The subclass will have access to attributes and methods from all its parents.
Multiple inheritance allows you to reuse code from several existing classes. However, you must manage the complexity of multiple inheritance with care. Otherwise, you can face issues like thediamond problem. You’ll learn more about this topic in theMethod Resolution Order (MRO) section.
Here’s a small example of multiple inheritance in Python:
crafts.py
classVehicle:def__init__(self,make,model,color):self.make=makeself.model=modelself.color=colordefstart(self):print("Starting the engine...")defstop(self):print("Stopping the engine...")defshow_technical_specs(self):print(f"Make:{self.make}")print(f"Model:{self.model}")print(f"Color:{self.color}")classCar(Vehicle):defdrive(self):print("Driving on the road...")classAircraft(Vehicle):deffly(self):print("Flying in the sky...")classFlyingCar(Car,Aircraft):pass
In this example, you write aVehicle
class with.make
,.model
, and.color
attributes. The class also has the.start()
,.stop()
, and.show_technical_specs()
methods. Then you create aCar
class that inherits fromVehicle
and extends it with a new method called.drive()
. You also create anAircraft
class that inherits fromVehicle
and adds a.fly()
method.
Finally, you define aFlyingCar
class to represent a car that you can drive on the road or fly in the sky. Isn’t that cool? Note that this class includes bothCar
andAircraft
in its list of parent classes. So, it’ll inherit functionality from both superclasses.
Here’s how you can use theFlyingCar
class:
>>>fromcraftsimportFlyingCar>>>space_flyer=FlyingCar("Space","Flyer","Black")>>>space_flyer.show_technical_specs()Make: SpaceModel: FlyerColor: Black>>>space_flyer.start()Starting the engine...>>>space_flyer.drive()Driving on the road...>>>space_flyer.fly()Flying in the sky...>>>space_flyer.stop()Stopping the engine...
In this code snippet, you first create an instance ofFlyingCar
. Then you call all its methods, including the inherited ones. As you can see, multiple inheritance promotes code reuse, allowing you to use functionality from several base classes at the same time. By the way, if you get thisFlyingCar
to really fly, then make sure you don’t stop the engine while you’re flying!
When you’re using multiple inheritance, you can face situations where one class inherits from two or more classes that have the same base class. This is known as thediamond problem. The real issue appears when multiple parents provide specific versions of the same method. In this case, it’d be difficult to determine which version of that method the subclass will end up using.
Python deals with this issue using a specificmethod resolution order (MRO). So, what is the method resolution order in Python? It’s an algorithm that tells Python how to search for inherited methods in a multiple inheritance context. Python’s MRO determines which implementation of a method or attribute to use when there are multiple versions of it in a class hierarchy.
Python’s MRO is based on the order of parent classes in the subclass definition. For example,Car
comes beforeAircraft
in theFlyingCar
class from the previous section. MRO also considers the inheritance relationships between classes. In general, Python searches for methods and attributes in the following order:
object
classIt’s important to note that the current class comes first in the search. Additionally, if you have multiple parents that implement a given method or attributes, then Python will search them in the same order that they’re listed in the class definition.
To illustrate the MRO, consider the following sample class hierarchy:
mro.py
classA:defmethod(self):print("A.method()")classB(A):defmethod(self):print("B.method()")classC(A):defmethod(self):print("C.method()")classD(B,C):pass
In this example,D
inherits fromB
andC
, which inherit fromA
. All the superclasses in the hierarchy define a different version of.method()
. Which of these versions willD
end up calling? To answer this question, go ahead and call.method()
on aD
instance:
>>>frommroimportD>>>D().method()B.method()
When you call.method()
on an instance ofD
, you getB.method
on your screen. This means that Python found.method()
on theB
class first. That’s the version of.method()
that you end up calling. You ignore the versions fromC
andA
.
Note: Sometimes, you may run into complex inheritance relationships where Python won’t be able to create a consistent method resolution order. In those cases, you’ll get aTypeError
pointing out the issue.
You can check the current MRO of a given class by using the.__mro__
special attribute:
>>>D.__mro__( <class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
In this output, you can see that Python searches for methods and attributes inD
by going throughD
itself, thenB
, thenC
, thenA
, and finally,object
, which is the base class of all Python classes.
The.__mro__
attribute can help you tweak your classes and define the specific MRO that you want your class to use. The way to tweak this is by moving and reordering the parent classes in the subclass definition until you get the desired MRO.
Amixin class provides methods that you can reuse in many other classes. Mixin classes don’t define new types, so they’re not intended to be instantiated but only inherited. You use their functionality to attach extra features to other classes quickly.
You can access the functionality of a mixin class in different ways. One of these ways is inheritance. However, inheriting from mixin classes doesn’t imply anis-a relationship because these classes don’t define concrete types. They just bundle specific functionality that’s intended to be reused in other classes.
To illustrate how to use mixin classes, say that you’re building a class hierarchy with aPerson
class at the top. From this class, you’ll derive classes likeEmployee
,Student
,Professor
, and several others. Then you realize that all the subclasses ofPerson
need methods thatserialize their data into different formats, includingJSON andpickle.
With this in mind, you think of writing aSerializerMixin
class that takes care of this task. Here’s what you come up with:
mixins.py
importjsonimportpickleclassPerson:def__init__(self,name,age):self.name=nameself.age=ageclassSerializerMixin:defto_json(self):returnjson.dumps(self.__dict__)defto_pickle(self):returnpickle.dumps(self.__dict__)classEmployee(SerializerMixin,Person):def__init__(self,name,age,salary):super().__init__(name,age)self.salary=salary
In this example,Person
is the parent class, andSerializerMixin
is a mixin class that provides serialization functionality. TheEmployee
class inherits from bothSerializerMixin
andPerson
. Therefore, it’ll inherit the.to_json()
and.to_pickle()
methods, which you can use to serialize instances ofEmployee
in your code.
In this example,Employee
is aPerson
. However, it’s not aSerializerMixin
because this class doesn’t define a type of object. It’s just a mixin class that packs serialization capabilities.
Note: Because of the method resolution order (MRO), which you learned about earlier, placing your mixin classesbefore the base classes on the list of parents is often necessary. It’s especially true for class-based views in theDjango web framework, which uses mixins to modify the behavior of a base view class.
Here’s howEmployee
works in practice:
>>>frommixinsimportEmployee>>>john=Employee("John Doe",30,50000)>>>john.to_json()'{"name": "John", "age": 30, "salary": 50000}'>>>john.to_pickle()b'...\x04name\x94\x8c\x08John Doe\x94\x8c\x03age\x94K\x1e\x8c\x06salary...'
Now yourEmployee
class is able to serialize its data using JSON and pickle formats. That’s great! Can you think of any other useful mixin classes?
Up to this point, you’ve learned a lot about simple and multiple inheritance in Python. In the following section, you’ll go through some of the advantages of using inheritance when writing and organizing your code.
Inheritance is a powerful tool that you can use to model and solve many real-world problems in your code. Some benefits of using inheritance include the following:
You can also use inheritance to define a uniform API for all the classes that belong to a given hierarchy. This promotes consistency and leverages polymorphism.
Using classes and inheritance, you can make your code more modular, reusable, and extensible. Inheritance enables you to apply good design principles, such asseparation of concerns. This principle states that you should organize code in small classes that each take care of a single task.
Even though inheritance comes with several benefits, it can also end up causing issues. If you overuse it or use it incorrectly, then you can:
Of course, these aren’t the only potential pitfalls. For example, having multiple levels of inheritance can make your code harder to reason about, which may impact your code’s maintainability in the long term.
Another drawback of inheritance is that inheritance is defined at compile time. So, there’s no way to change the inherited functionality at runtime. Other techniques, like composition, allow you to dynamically change the functionality of a given class by replacing its components.
Inheritance, and especially multiple inheritance, can be a complex and hard-to-grasp topic. Fortunately, inheritance isn’t the only technique that allows you to reuse functionality in object-oriented programming. You also havecomposition, which represents ahas-a relationship between classes.
Composition allows you to build an object from its components. Thecomposite object doesn’t have direct access to each component’s interface. However, it can leverage each component’s implementation.
Delegation is another technique that you can use to promote code reuse in your OOP code. With delegation, you can representcan-do relationships, where an object relies on another object to perform a given task.
In the following sections, you’ll learn more about these techniques and how they can make your object-oriented code more robust and flexible.
As you’ve already learned, you can usecomposition to model ahas-a relationship between objects. In other words, through composition, you can create complex objects by combining objects that will work ascomponents. Note that these components may not make sense as stand-alone classes.
Favoring composition over inheritance leads to more flexible class designs. Unlike inheritance, composition is defined at runtime, which means that you can dynamically replace a current component with another component of the same type. This characteristic makes it possible to change the composite’s behavior at runtime.
In the example below, you use composition to create anIndustrialRobot
class from theBody
andArm
components:
robot.py
classIndustrialRobot:def__init__(self):self.body=Body()self.arm=Arm()defrotate_body_left(self,degrees=10):self.body.rotate_left(degrees)defrotate_body_right(self,degrees=10):self.body.rotate_right(degrees)defmove_arm_up(self,distance=10):self.arm.move_up(distance)defmove_arm_down(self,distance=10):self.arm.move_down(distance)defweld(self):self.arm.weld()classBody:def__init__(self):self.rotation=0defrotate_left(self,degrees=10):self.rotation-=degreesprint(f"Rotating body{degrees} degrees to the left...")defrotate_right(self,degrees=10):self.rotation+=degreesprint(f"Rotating body{degrees} degrees to the right...")classArm:def__init__(self):self.position=0defmove_up(self,distance=1):self.position+=distanceprint(f"Moving arm{distance} cm up...")defmove_down(self,distance=1):self.position-=distanceprint(f"Moving arm{distance} cm down...")defweld(self):print("Welding...")
In this example, you build anIndustrialRobot
class out of its components,Body
andArm
. TheBody
class provides horizontal movements, while theArm
class represents the robot’s arm and provides vertical movement and welding functionality.
Here’s how you can useIndustrialRobot
in your code:
>>>fromrobotimportIndustrialRobot>>>robot=IndustrialRobot()>>>robot.rotate_body_left()Rotating body 10 degrees to the left...>>>robot.move_arm_up(15)Moving arm 15 cm up...>>>robot.weld()Welding...>>>robot.rotate_body_right(20)Rotating body 20 degrees to the right...>>>robot.move_arm_down(5)Moving arm 5 cm down...>>>robot.weld()Welding...
Great! Your robot works as expected. It allows you to move its body and arm according to your movement needs. It also allows you to weld different mechanical pieces together.
An idea to make this robot even cooler is to implement several types of arms with different welding technologies. Then you can change the arm by doing something likerobot.arm = NewArm()
. You can even add a.change_arm()
method to your robot class. How does that sound as a learning exercise?
Unlike inheritance, composition doesn’t expose the entire interface of components, so it preserves encapsulation. Instead, the composite objects access and use only the required functionality from their components. This characteristic makes your class design more robust and reliable because it won’t expose unneeded members.
Following the robot example, say you have several different robots in a factory. Each robot can have different capabilities like welding, cutting, shaping, polishing, and so on. You also have several independent arms. Some of them can perform all those actions. Some of them can perform just a subset of the actions.
Now say that a given robot can only weld. However, this robot can use different arms with different welding technologies. If you use inheritance, then the robot will have access to other operations like cutting and shaping, which can cause an accident or breakdown.
If you use composition, then the welder robot will only have access to the arm’s welding feature. That said, composition can help you protect your classes from unintended use.
Delegation is another technique that you can use as an alternative to inheritance. With delegation, you can modelcan-do relationships, where an object hands a task over to another object, which takes care of executing the task. Note that the delegated object can exist independently from the delegator.
You can use delegation to achieve code reuse, separation of concerns, and modularity. For example, say that you want to create astack data structure. You think of taking advantage of Python’slist
as a quick way to store and manipulate the underlying data.
Here’s how you end up writing yourStack
class:
stack.py
classStack:def__init__(self,items=None):ifitemsisNone:self._items=[]else:self._items=list(items)defpush(self,item):self._items.append(item)defpop(self):returnself._items.pop()def__repr__(self)->str:returnf"{type(self).__name__}({self._items})"
In.__init__()
, you define alist
object called._items
that can take its initial data from theitems
argument. You’ll use this list to store the data in the containingStack
, so you delegate all the operations related to storing, adding, and deleting data to this list object. Then you implement the typicalStack
operations,.push()
and.pop()
.
Note how these operations conveniently delegate their responsibilities on._items.append()
and._items.pop()
, respectively. YourStack
class has handed its operations over to thelist
object, which already knows how to perform them.
It’s important to notice that this class is pretty flexible. You can replace the list object in._items
with any other object as long as it implements the.pop()
and.append()
methods. For example, you can use adeque
object from thecollections
module.
Because you’ve used delegation to write your class, the internal implementation oflist
isn’t visible or directly accessible inStack
, which preserves encapsulation:
>>>fromstackimportStack>>>stack=Stack([1,2,3])>>>stackStack([1, 2, 3])>>>stack.push(4)>>>stackStack([1, 2, 3, 4])>>>stack.pop()>>>stack.pop()>>>stackStack([1, 2])>>>dir(stack)[ ... '_items', 'pop', 'push']
The public interface of yourStack
class only contains the stack-related methods.pop()
and.push()
, as you can see in thedir()
function’s output. This prevents the users of your class from using list-specific methods that aren’t compatible with the classic stack data structure.
If you use inheritance, then your child class,Stack
, will inherit all the functionality from its parent class,list
:
>>>classStack(list):...defpush(self,item):...self.append(item)...defpop(self):...returnsuper().pop()...def__repr__(self)->str:...returnf"{type(self).__name__}({super().__repr__()})"...>>>stack=Stack()>>>dir(stack)[ ... 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'push', 'remove', 'reverse', 'sort']
In this example, yourStack
class has inherited all the methods fromlist
. These methods are exposed as part of your class’s public API, which may lead to incorrect uses of the class and its instances.
With inheritance, the internals of parent classes are visible to subclasses, which breaks encapsulation. If some of the parent’s functionality isn’t appropriate for the child, then you run the risk of incorrect use. In this situation, composition and delegation are safer options.
Note: To learn more about creating custom list-like classes, check out theCustom Python Lists: Inheriting Fromlist
vsUserList
tutorial.
Finally, in Python, you can quickly implement delegation through the.__getattr__()
special method. Python calls this method automatically whenever you access an instance attribute or method. You can use this method to redirect the request to another object that can provide the appropriate method or attribute.
To illustrate this technique, get back to the mixin example where you used a mixin class to provide serialization capabilities to yourEmployee
class. Here’s how to rewrite the example using delegation:
serializer_delegation.py
importjsonimportpickleclassPerson:def__init__(self,name,age):self.name=nameself.age=ageclassSerializer:def__init__(self,instance):self.instance=instancedefto_json(self):returnjson.dumps(self.instance.__dict__)defto_pickle(self):returnpickle.dumps(self.instance.__dict__)classEmployee(Person):def__init__(self,name,age,salary):super().__init__(name,age)self.salary=salarydef__getattr__(self,attr):returngetattr(Serializer(self),attr)
In this new implementation, the serializer class takes the instance that provides the data as an argument.Employee
defines a.__getattr__()
method that uses the built-ingetattr()
function to access the methods in theSerializer
class.
For example, if you call.to_json()
on an instance ofEmployee
, then that call will be automatically redirected to calling.to_json()
on the instance ofSerializer
. Go ahead and try it out! This is a pretty cool Python feature.
You’ve tried your hand at a quick example of delegation in Python to learn how a class can delegate some of its responsibilities to another class, achieving code reuse and separation of concerns. Again, you should note that this technique indirectly exposes all the delegated attributes and methods. So, use it with care.
Dependency injection is adesign pattern that you can use to achieve loosecoupling between a class and its components. With this technique, you can provide an object’s dependencies from the outside, rather than inheriting or implementing them in the object itself. This practice allows you to create flexible classes that are able to change their behavior dynamically, depending on the injected functionality.
In your robot example, you can use dependency injection to decouple theArm
andBody
classes fromIndustrialRobot
, which will make your code more flexible and versatile.
Here’s the updated example:
robot.py
classIndustrialRobot:def__init__(self,body,arm):self.body=bodyself.arm=arm# ...# ...
In this new version ofIndustrialRobot
, you only made two small changes to.__init__()
. Now this method takesbody
andarm
as arguments and assigns their values to the corresponding instance attributes,.body
and.arm
. This allows you to inject appropriate body and arm objects into the class so that it can do its work.
Here’s how you can useIndustrialRobot
with this new implementation:
>>>fromrobotimportArm,Body,IndustrialRobot>>>robot=IndustrialRobot(Body(),Arm())>>>robot.rotate_body_left()Rotating body 10 degrees to the left...>>>robot.move_arm_up(15)Moving arm 15 cm up...>>>robot.weld()Welding...>>>robot.rotate_body_right(20)Rotating body 20 degrees to the right...>>>robot.move_arm_down(5)Moving arm 5 cm down...>>>robot.weld()Welding...
Overall, the class’s functionality remains the same as in your first version. The only difference is that now you have to pass the body and arm objects to the class constructor. This step is a common way of implementing dependency injection.
Now that you know about a few techniques that you can use as alternatives to inheritance, it’s time for you to learn aboutabstract base classes (ABCs) in Python. These classes allow you to define consistent APIs for your classes.
Sometimes, you want to create a class hierarchy in which all the classes implement a predefinedinterface or API. In other words, you want to define the specific set of public methods and attributes that all the classes in the hierarchy must implement. In Python, you can do this using what’s known as anabstract base class (ABC).
Theabc
module in the standard library exports a couple of ABCs and other related tools that you can use to define custom base classes that require all their subclasses to implement specific interfaces.
You can’t instantiate ABCs directly. You must subclass them. In a sense, ABCs work as templates for other classes to inherit from.
To illustrate how to use Python’s ABCs, say that you want to create a class hierarchy to represent different shapes, such asCircle
,Square
, and so on. You decide that all the classes should have the.get_area()
and.get_perimeter()
methods. In this situation, you can start with the following base class:
shapes_abc.py
fromabcimportABC,abstractmethodclassShape(ABC):@abstractmethoddefget_area(self):pass@abstractmethoddefget_perimeter(self):pass
TheShape
class inherits fromabc.ABC
, which means it’s an abstract base class. Then you define the.get_area()
and.get_perimeter()
methods using the@abstractmethod
decorator. By using this decorator, you declare that these two methods are the common interface that all the subclasses ofShape
must implement.
Now you can create theCircle
class. Here’s the first approach to this class:
shapes_abc.py
fromabcimportABC,abstractmethodfrommathimportpi# ...classCircle(Shape):def__init__(self,radius):self.radius=radiusdefget_area(self):returnpi*self.radius**2
In this code snippet, you define theCircle
class by inheriting fromShape
. At this point, you’ve added the.get_area()
method only. Now go ahead and run the following code:
>>>fromshapes_abcimportCircle>>>circle=Circle(100)Traceback (most recent call last):...TypeError:Can't instantiate abstract class Circle with abstract method get_perimeter
What just happened? You can’t instantiateCircle
. That’s what ABCs are for. To be able to instantiateCircle
, you must provide suitable implementations for all its abstract methods, which means you need to define the complete interface:
shapes_abc.py
fromabcimportABC,abstractmethodfrommathimportpi# ...classCircle(Shape):def__init__(self,radius):self.radius=radiusdefget_area(self):returnpi*self.radius**2defget_perimeter(self):return2*pi*self.radius
This time, yourCircle
class implements all the required methods. These methods will be common to all the classes in your shape hierarchy. Once you’ve defined suitable custom implementations for all the abstract methods, you can proceed to instantiateCircle
as in the following example:
>>>fromshapes_abcimportCircle>>>circle=Circle(100)>>>circle.radius100>>>circle.get_area()31415.926535897932>>>circle.get_perimeter()628.3185307179587
Once you’ve implemented custom methods to replace the abstract implementations of.get_area()
and.get_perimeter()
, then you can instantiate and useCircle
in your code.
If you want to add aSquare
class to your shape hierarchy, then that class must have custom implementations of the.get_area()
and.get_perimeter()
methods:
shapes_abc.py
fromabcimportABC,abstractmethodfrommathimportpi# ...classSquare(Shape):def__init__(self,side):self.side=sidedefget_area(self):returnself.side**2defget_perimeter(self):return4*self.side
This example demonstrates how you can use ABCs to define a common interface for a group of related classes. Each subclass must provide its own implementation of the abstract methods in the base class. Note that in recent Python versions, you can usestatic duck typing as an alternative to abstract base classes.
In the previous section, you learned about abstract base classes and explored how to use them to promote the use of a common public interface across several related classes. Having a set of classes to implement the same interface with specific behaviors for concrete classes is a great way to unlockpolymorphism.
Polymorphism is when you can use objects of different classes interchangeably because they share a common interface. For example, Pythonstrings,lists, andtuples are allsequence data types. This means that they implement an interface that’s common to all sequences.
Because of this common interface, you can use them in similar ways. For example, you can:
.__iter__()
method.__getitem__()
method.__len__()
methodThese are just a few examples of common features of sequence data types. Note that you can run all these operations and more without caring about which specific type you’re actually using in your code. That’s possible because of polymorphism.
Consider the following examples, which use the built-inlen()
function:
>>>message="Hello!">>>numbers=[1,2,3]>>>letters=("A","B","C")>>>len(message)6>>>len(numbers)3>>>len(letters)3
In these examples, you uselen()
with three different types of objects: a string, a list, and a tuple. Even though these types are quite different, all of them implement the.__len__()
method, which provides support forlen()
.
You can unlock polymorphism in your classes by making them share common attributes and methods.
Note: In Python, this type of polymorphism is commonly known asduck typing. To learn more about it, check out theDuck Typing in Python: Writing Flexible and Decoupled Code tutorial.
For example, take a look at yourVehicle
class hierarchy. TheCar
class has a method called.drive()
, and theMotorcycle
class has a method called.ride()
. This API inconsistency breaks polymorphism. To fix the issue and use these classes in a polymorphic way, you can slightly changeMotorcycle
by renaming its.ride()
method to.drive()
:
vehicles.py
# ...classMotorcycle(Vehicle):def__init__(self,make,model,year,num_wheels):super().__init__(make,model,year)self.num_wheels=num_wheelsdefdrive(self):print(f'Riding my "{self.make} -{self.model}" on the road')def__str__(self):returnf'"{self.make} -{self.model}" has{self.num_wheels} wheels'
Here, you rename the.ride()
method to.drive()
in the definition ofMotorcycle
. This small change makes your classes have a common interface. So, you can use them interchangeably in your code:
>>>fromvehiclesimportCar,Motorcycle>>>toyota=Car("Toyota","Corolla",2022,5)>>>honda=Car("Honda","Civic",2022,4)>>>harley=Motorcycle("Harley-Davidson","Iron 883",2022,2)>>>indian=Motorcycle("Indian","Scout",2022,2)>>>forvehiclein[toyota,honda,harley,indian]:...vehicle.drive()...Driving my "Toyota - Corolla" on the roadDriving my "Honda - Civic" on the roadRiding my "Harley-Davidson - Iron 883" on the roadRiding my "Indian - Scout" on the road
Now you can drive either a car or a motorcycle without having to worry about anAttributeError
because one of them doesn’t have the appropriate method. You’ve just made your classes work in a polymorphic way, which is a great way to add flexibility to your code.
You now know a lot about Pythonclasses and how to use them to make your code more reusable, modular, flexible, and maintainable. Classes are the building blocks of object-oriented programming in Python. With classes, you can solve complex problems by modeling real-world objects, their properties, and their behaviors. Classes provide an intuitive and human-friendly approach to complex programming problems, which will make your life more pleasant.
In this tutorial, you’ve learned how to:
class
keywordWith all this knowledge, you can leverage the power of Python classes in your code. Now you’re ready to start writing your own classes in Python.
Get Your Code:Click here to download your free sample code that shows you how to build powerful object blueprints with classes in Python.
Now that you have some experience with classes in Python, you can use the questions and answers below to check your understanding and recap what you’ve learned.
These FAQs are related to the most important concepts you’ve covered in this tutorial. Click theShow/Hide toggle beside each question to reveal the answer.
You define a class in Python using theclass
keyword followed by the class name and a colon. Inside the class body, you define attributes and methods that represent the data and behavior of the class.
A class in Python is like a blueprint that defines the data and behavior of objects, while an object is an instance of a class with specific values for its attributes.
Methods in Python classes are functions defined within a class that operate on its attributes, while attributes are variables within a class that hold data specific to each object.
You should use classes in Python when you need to encapsulate data and behavior together, model real-world entities, or organize complex code into reusable and maintainable structures.
Instance attributes are tied to a specific instance of a class and hold data unique to that instance, while class attributes are shared across all instances of a class and hold data common to the class.
Take the Quiz: Test your knowledge with our interactive “Python Classes - The Power of Object-Oriented Programming” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
Python Classes - The Power of Object-Oriented ProgrammingIn this quiz, you'll test your understanding of Python classes. With this knowledge, you'll be able to define reusable pieces of code that encapsulate data and behavior in a single entity, model real-world objects, and solve complex problems.
Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding:Inheritance and Internals: Object-Oriented Programming in Python
🐍 Python Tricks 💌
Get a short & sweetPython Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.
AboutLeodanis Pozo Ramos
Leodanis is an industrial engineer who loves Python and software development. He's a self-taught Python developer with 6+ years of experience. He's an avid technical writer with a growing number of articles published on Real Python and other sites.
» More about LeodanisMasterReal-World Python Skills With Unlimited Access to Real Python
Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:
MasterReal-World Python Skills
With Unlimited Access to Real Python
Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:
What Do You Think?
What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.
Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students.Get tips for asking good questions andget answers to common questions in our support portal.
Keep Learning
Related Topics:intermediatepython
Recommended Video Course:Inheritance and Internals: Object-Oriented Programming in Python
Related Tutorials:
Already have an account?Sign-In
Almost there! Complete this form and click the button below to gain instant access:
Python Classes: The Power of Object-Oriented Programming (Sample Code)