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 supports the object-oriented programming paradigm through classes. They provide an elegant way to define reusable pieces of code that encapsulate data and behavior in a single entity. With classes, you can quickly and intuitively model real-world objects and solve complex problems.
If you’re new to classes, need to refresh your knowledge, or want to dive deeper into them, then this tutorial is for you!
In this tutorial, you’ll learn how to:
- Define Python classes with the
classkeyword - Add state to your classes with class and instance attributes
- Provide behavior to your classes with methods
- Use inheritance to build hierarchies of classes
- Provide interfaces with abstract classes
To get the most out of this tutorial, you should know about Python variables, data types, and functions. Some experience with object-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.
Free Bonus: Click here to download your sample code for building 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. Upon completion you will receive a score so you can track your learning progress over time:
Getting Started With Python Classes
Python is a multiparadigm programming language that supports object-oriented programming (OOP) through classes that you can define with the class keyword. You can think of a class as a piece of code that specifies the data and behavior 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 an object or instance 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’s state. 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 term attributes to refer to the properties or data associated with a specific object of a given class. In Python, attributes are variables defined inside a class with the purpose of storing all the required data for the class to work.
Similarly, you’ll use the term methods 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 as members of a class or object.
You can write fully functional 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 build class 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.
Defining a Class in Python
To define a class, you need to use the class keyword followed by the class name and a colon, just like you’d do for other compound statements in Python. Then you must define the class body, which will start at the next indentation level:
class ClassName:
# Class body
pass
In a class 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 a namespace 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 a Circle 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
import math
class Circle:
def __init__(self, radius):
self.radius = radius
def calculate_area(self):
return round(math.pi * self.radius ** 2, 2)
In this code snippet, you define Circle using the class keyword. Inside the class, you write two methods. The .__init__() method has a special meaning in Python classes. This method is known as the object initializer because it defines and sets the initial values for your attributes. You’ll learn more about this method in the Instance Attributes section.
The second method of Circle is conveniently named .calculate_area() and will compute the area of a specific circle by using its radius. It’s common for method names to contain a verb, such as calculate, to describe an action the method performs. In this example, you’ve used the math module to access the pi constant as it’s defined in that module.
Note: In Python, the first argument of most methods is self. 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 on instance methods with self.
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.
Creating Objects From a Class in Python
The action of creating concrete objects from an existing class is known as instantiation. 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 of Circle by running the following code in a Python REPL session:
>>> from circle import Circle
>>> 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 like Circle, you must call the Circle() 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, the Circle class expects the radius 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 and circle_2 are separate instances of Circle. 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.
Accessing Attributes and Methods
In Python, you can access the attributes and methods of an object by using dot notation with the dot operator. The following snippet of code shows the required syntax:
obj.attribute_name
obj.method_name()
Note that the dot (.) in this syntax basically means give 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 so that you can call 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:
>>> from circle import Circle
>>> circle_1 = Circle(42)
>>> circle_2 = Circle(7)
>>> circle_1.radius
42
>>> circle_1.calculate_area()
5541.77
>>> circle_2.radius
7
>>> circle_2.calculate_area()
153.94
In the first couple of lines after the instantiation, you access the .radius attribute on your circle_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 the circle_2 object.
You can also use dot notation and an assignment statement to change the current value of an attribute:
>>> circle_1.radius = 100
>>> circle_1.radius
100
>>> circle_1.calculate_area()
31415.93
Now the radius of circle_1 is entirely different. When you call .calculate_area(), the result immediately reflects this change. You’ve changed the object’s internal state or data, which typically impacts its behaviors or methods.
Naming Conventions in Python Classes
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 explicit restrictions. Because of that, the language and the community rely on conventions rather than restrictions.
Note: Most Python programmers follow the snake_case naming convention, which involves using underscores (_) to separate multiple words. However, the recommended naming convention for Python classes is the PascalCase, where each word is capitalized.
In the following two sections, you’ll learn about two important naming conventions that apply to class attributes.
Public vs Non-Public Members
The first naming convention that you need to know about is related to the fact that Python doesn’t distinguish between private, protected, and public attributes like Java 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 a leading underscore to the member’s name. So, in a Python class, you’ll have the following convention:
| 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 official interface or API of your classes, while non-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 only indicates 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 obj._name, and you’ll 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 may be removed at any time, so you shouldn’t rely on them. 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 at any moment.
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.
Name Mangling
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 as name 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 results in name hiding. In other words, mangled names aren’t available for direct access. They’re not part of a class’s public API.
For example, consider the following sample class:
>>> class SampleClass:
... 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.__value
Traceback (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 an AttributeError.
Note: In the above example, you use the built-in vars() 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.
This internal behavior 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 named-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.
Name mangling is particularly useful when you want to ensure that a given attribute or method won’t get accidentally overwritten. It’s a way to avoid naming conflicts between classes or subclasses. It’s also useful to prevent subclasses from overriding methods that have been optimized for better performance.
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 jump into some reasons why you should learn about classes and use them in your Python projects.
Understanding the Benefits of Using Classes in Python
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 intuitive interfaces (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’ll overcomplicate your solutions.
Note: In Python, the public attributes and methods of a class make up what you’ll know as the class’s interface or application 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.
Deciding When to Avoid Using Classes
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 a couple of functions are 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:
- Store only data. Use a data class or a named tuple instead.
- Provide a single method. Use a function instead.
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 the Python principle:
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 general situations where you may not need to use classes in your Python code. For example, classes aren’t necessary when you’re working with:
-
A small and simple program or script that doesn’t require complex data structures or logic. In this case, using classes may be overkill.
-
A performance-critical program. Classes add overhead to your program, especially when you need to create many objects. This may affect your code’s general performance.
-
A legacy codebase. If an existing codebase doesn’t use classes, then you shouldn’t introduce them. This will break the current coding style and disrupt the code’s consistency.
-
A team with a different coding style. If your current team doesn’t use classes, then stick with their coding style. This will ensure consistency across the project.
-
A codebase that uses functional programming. If a given codebase is currently written with a functional approach, then you shouldn’t introduce classes. This will break the underlying coding paradigm.
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.
Attaching Data to Classes and Instances
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:
- Class attributes: A class attribute is a variable that you define in the class body directly. Class attributes belong to their containing class. Their data is common to the class and all its instances.
- Instance attributes: An instance is a variable that you define inside a method. 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
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:
>>> class ObjectCounter:
... 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_instances
4
>>> counter = ObjectCounter()
>>> counter.num_instances
5
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-in type() function is best because it’ll make your class more flexible:
>>> class ObjectCounter:
... num_instances = 0
... def __init__(self):
... type(self).num_instances += 1
...
The built-in type() function returns the class or type of self, which is ObjectCounter in this example. This subtle change makes your class more robust and reliable by avoiding hard-coding the class that provides the attribute.
It’s important to note that you can access class attributes using either the class or one of its instances. That’s why you can use the counter object to retrieve the value of .num_instances. However, if you need to modify a class attribute, then you must use the class itself rather than one of its instances.
For example, if you use self to modify .num_instances, then you’ll be overriding the original class attribute by creating a new instance attribute:
>>> class ObjectCounter:
... 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_instances
0
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 why ObjectCounter.num_instances returns 0 in this example. You’ve overridden the class attribute in the highlighted line.
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
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 inside instance methods, which are those methods that receive self 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 the correct values when you create a new instance. Additionally, it makes the code more organized and easier to debug.
Consider the following Car class, which defines a bunch of instance attributes:
# car.py
class Car:
def __init__(self, make, model, year, color):
self.make = make
self.model = model
self.year = year
self.color = color
self.started = False
self.speed = 0
self.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 the self argument. This argument holds a reference to the current instance, which is where the attributes belong and live. The self argument plays a fundamental role in Python classes. You’ll learn more about self in the section Instance Methods With self.
Here’s how your Car class works in practice:
>>> from car import Car
>>> toyota_camry = Car("Toyota", "Camry", 2022, "Red")
>>> toyota_camry.make
'Toyota'
>>> toyota_camry.model
'Camry'
>>> toyota_camry.color
'Red'
>>> toyota_camry.speed
0
>>> ford_mustang = Car("Ford", "Mustang", 2022, "Black")
>>> ford_mustang.make
'Ford'
>>> ford_mustang.model
'Mustang'
>>> ford_mustang.year
2022
>>> ford_mustang.max_speed
200
In these examples, you create two different instances of Car. 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.make
Traceback (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 an AttributeError exception.
The .__dict__ Attribute
In Python, both classes and instances have a special attribute called .__dict__. This attribute holds a dictionary 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 an AttributeError.
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 appears there, then Python looks in the class .__dict__. If the name isn’t found, then you get a NameError.
Here’s a toy class that illustrates how this mechanism works:
# sample_dict.py
class SampleClass:
class_attr = 100
def __init__(self, instance_attr):
self.instance_attr = instance_attr
def method(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 of 100. 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:
>>> from sample_dict import SampleClass
>>> SampleClass.class_attr
100
>>> 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 a dictionary.
Note: You can access the same dictionary by calling the built-in vars() 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: 100
Instance 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 avoid RecursionError exceptions when you’re wiring descriptors in Python. You’ll learn more about descriptors in the Property and Descriptor-Based Attributes section.
Dynamic Class and Instance Attributes
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 a CSV file:
>>> class Record:
... """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 an employees.csv file using csv.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 your Record class, and you need to represent each data field as an instance attribute. Here’s how you can do it:
>>> john_record = Record()
>>> for field, value in john.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 of Record called john_record. Then you run a for loop to iterate over the items of your dictionary of data, john. Inside the loop, you use the built-in setattr() function to sequentially add each field as an attribute to your john_record object. If you inspect john_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:
>>> class User:
... 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 minimal User class with no custom attributes or methods. To define the class’s body, you’ve just used a pass statement as a placeholder, which is Python’s way of doing nothing.
Then you create an object called jane. 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 the User class with an initializer or .__init__() method. In this method, you take the name and job arguments, which you turn into instance attributes in the method’s body. Then you add the method to User dynamically. After this addition, you can create User 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.
Property and Descriptor-Based Attributes
Python allows you to add function-like behavior on top of existing instance attributes and turn them into managed 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 out Python’s property(): Add Managed Attributes to Your Classes.
As an example, get back to your Circle 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 the setter method.
Here’s what your new version of Circle can look like:
# circle.py
import math
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if not isinstance(value, int | float) or value <= 0:
raise ValueError("positive number expected")
self._radius = value
def calculate_area(self):
return round(math.pi * self._radius**2, 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.
To define the setter method of a property-based attribute, you need to use the decorator @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 to 0. If either is true, then you raise a ValueError with a descriptive message about the actual issue. Finally, you assign value to ._radius, and that’s it. Now, .radius is a property-based attribute.
Here’s an example of this new version of Circle in action:
>>> from circle import Circle
>>> circle_1 = Circle(100)
>>> circle_1.radius
100
>>> circle_1.radius = 500
>>> circle_1.radius = 0
Traceback (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 of Circle 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 a ValueError.
Note: Remember to reload the circle.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 of Circle. 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 out Python 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 following Square class:
# square.py
class Square:
def __init__(self, side):
self.side = side
@property
def side(self):
return self._side
@side.setter
def side(self, value):
if not isinstance(value, int | float) or value <= 0:
raise ValueError("positive number expected")
self._side = value
def calculate_area(self):
return round(self._side**2, 2)
This class uses the same pattern as your Circle class. Instead of using radius, the Square class takes a side argument and computes the area using the appropriate expression for a square.
This class is pretty similar to Circle, 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
import math
class PositiveNumber:
def __set_name__(self, owner, name):
self._name = name
def __get__(self, instance, owner):
return instance.__dict__[self._name]
def __set__(self, instance, value):
if not isinstance(value, int | float) or value <= 0:
raise ValueError("positive number expected")
instance.__dict__[self._name] = value
class Circle:
radius = PositiveNumber()
def __init__(self, radius):
self.radius = radius
def calculate_area(self):
return round(math.pi * self.radius**2, 2)
class Square:
side = PositiveNumber()
def __init__(self, side):
self.side = side
def calculate_area(self):
return round(self.side**2, 2)
The first thing to notice in this example is that you moved all the classes to a shapes.py file. In that file, you define a descriptor class called PositiveNumber by implementing the .__get__() and .__set__() special methods, which are part of the descriptor protocol.
Next, you remove the .radius property from Circle and the .side property from Square. In Circle, you add a .radius class attribute, which holds an instance of PositiveNumber. You do something similar in Square, but the class attribute is appropriately named .side.
Here are a few examples of how your classes work now:
>>> from shapes import Circle, Square
>>> circle = Circle(100)
>>> circle.radius
100
>>> circle.radius = 500
>>> circle.radius
500
>>> circle.radius = 0
Traceback (most recent call last):
...
ValueError: positive number expected
>>> square = Square(200)
>>> square.side
200
>>> square.side = 300
>>> square.side
300
>>> square.side = -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.
Lightweight Classes With .__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 a Point class with a .__slots__ attribute that consists of a tuple of allowed attributes. Each attribute will represent a Cartesian coordinate:
>>> class Point:
... __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__'
This Point 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 Python identifiers.
Note: Although .__slots__ can hold a list object, you should use a tuple object instead. Even if changing the list in .__slots__ after processing the class body had no effect, it’d be misleading to use a mutable sequence there.
Instances of your Point 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 of Point. To do this, you can use the Pympler library, which you can install from PyPI using the pip install pympler command.
Once you’ve installed Pympler with pip, then you can run the following code in your REPL:
>>> from pympler import asizeof
>>> asizeof.asizeof(Point(4, 8))
112
The asizeof() function from Pympler says that the object Point(4, 8) occupies 112 bytes in your computer’s memory. Now get back to your REPL session and redefine Point without providing a .__slots__ attribute. With this update in place, go ahead and run the memory check again:
>>> class Point:
... 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 of Point. 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:
>>> class Point:
... __slots__ = ("x", "y")
... def __init__(self, x, y):
... self.x = x
... self.y = y
...
>>> point = Point(4, 8)
>>> point.z = 16
Traceback (most recent call last):
...
AttributeError: 'Point' object has no attribute 'z'
Adding a .__slots__ to your classes allows you to provide a series of allowed 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 an AttributeError 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.
Providing Behavior With Methods
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 and return 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:
- Instance methods, which take the current instance,
self, as their first argument - Class methods, which take the current class,
cls, as their first argument - Static methods, which take neither the class nor the instance
Every 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 out Python’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.
Instance Methods With 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 called self by convention.
Note: Naming the current instance self is a strong convention in Python. It’s so strong that it may look like self is one of the Python keywords. However, you could use any other name instead of self.
Even though it’s possible to use any name for the first argument of an instance method, using self is definitely the right choice because it’ll make your code look like Python code in the eyes of other developers.
The self argument holds a reference to the current instance, allowing you to access that instance from within methods. More importantly, through self, 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 accepts self as its first argument. Up to this point, you’ve already written some instance methods. To continue learning about them, turn back to your Car 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
class Car:
def __init__(self, make, model, year, color):
self.make = make
self.model = model
self.year = year
self.color = color
self.started = False
self.speed = 0
self.max_speed = 200
def start(self):
print("Starting the car...")
self.started = True
def stop(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 use self to access the .started attribute on the current instance using dot notation. Then you change the current value of this attribute to True in .start() and to False 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 use self in its body, then that may not be an instance method. In this case, you should probably use a class method or a static method, depending on your specific needs.
Now you can add the .accelerate() and .brake() methods, which will be a bit more complex:
# car.py
class Car:
def __init__(self, make, model, year, color):
self.make = make
self.model = model
self.year = year
self.color = color
self.started = False
self.speed = 0
self.max_speed = 200
# ...
def accelerate(self, value):
if not self.started:
print("Car is not started!")
return
if self.speed + value <= self.max_speed:
self.speed += value
else:
self.speed = self.max_speed
print(f"Accelerating to {self.speed} km/h...")
def brake(self, value):
if self.speed - value >= 0:
self.speed -= value
else:
self.speed = 0
print(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 to 0 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:
>>> from car import Car
>>> 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! Your Car 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 like ford_mustang using dot notation, you don’t have to provide a value for the self argument. Python takes care of that step for you. It automatically passes the target instance to self. 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 the self argument. In this example, that value is the ford_mustang instance. Note that if you don’t provide a suitable instance, then the call fails with a TypeError. The error message is pretty clear. There’s a missing positional argument, self.
Special Methods and Protocols
Python supports what it calls special methods, which are also known as dunder or magic 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 (double underscore). 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.
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 provide string representations for your objects.
Go ahead and update your Car class to add these two methods:
# car.py
class Car:
# ...
def __str__(self):
return f"{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 the informal 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 either str() or print().
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 the formal 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. In script mode, you can access an object’s formal string representation using the built-in repr() function.
Run the following code to give your new methods a try. Remember that you need to restart your REPL or reload car.py:
>>> from car import Car
>>> toyota_camry = Car("Toyota", "Camry", 2022, "Red")
>>> str(toyota_camry)
'Toyota, Camry, Red: (2022)'
>>> print(toyota_camry)
Toyota, Camry, Red: (2022)
>>> toyota_camry
Car(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 of Car as an argument to str() or print(), 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 of Car 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.
Python protocols 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 on with 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 the Property and Descriptor-Based Attributes section.
Here’s an example of a minimal ThreeDPoint class that implements the iterable protocol:
>>> class ThreeDPoint:
... 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 a generator function that returns an iterator. The resulting iterator yields the coordinates of ThreeDPoint on demand.
The call to list() iterates over the attributes .x, .y, and .z, returning a list object. You don’t need to call .__iter__() directly. Python calls it automatically when you use an instance of ThreeDPoint in an iteration.
Class Methods With @classmethod
You can also add class methods to your custom Python classes. A class method is a method that takes the class object as its first argument instead of taking self. In this case, the argument should be called cls, 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 with multiple 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 your ThreeDPoint so that you can quickly create points from tuples or lists of coordinates:
# point.py
class ThreeDPoint:
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)
@classmethod
def from_sequence(cls, sequence):
return cls(*sequence)
def __repr__(self):
return f"{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 a ThreeDPoint object from it, and return the object to the caller. To create the new object, you use the cls argument, which holds an implicit reference to the current class, which Python injects into your method automatically.
Here’s how this class method works:
>>> from point import ThreeDPoint
>>> 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 the ThreeDPoint class directly to access the class method .from_sequence(). Note that you can also access the method using a concrete instance, like point in the example. In each of the calls to .from_sequence(), you’ll get a completely new instance of ThreeDPoint. However, class methods should be accessed through the corresponding class name for better clarity and to avoid confusion.
Static Methods With @staticmethod
Your Python classes can also have static 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
class ThreeDPoint:
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)
@classmethod
def from_sequence(cls, sequence):
return cls(*sequence)
@staticmethod
def show_intro_message(name):
print(f"Hey {name}! This is your 3D Point!")
def __repr__(self):
return f"{type(self).__name__}({self.x}, {self.y}, {self.z})"
The .show_intro_message() static method takes a name 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:
>>> from point import ThreeDPoint
>>> 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.
Getter and Setter Methods vs Properties
Programming languages like Java and C++ 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 out Getters and Setters: Manage Attributes in Python.
Using methods to access and update attributes promotes encapsulation. 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 a Person 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
class Person:
def __init__(self, name):
self.set_name(name)
def get_name(self):
return self._name
def set_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 your Person class:
>>> from person import Person
>>> jane = Person("Jane")
>>> jane.get_name()
'Jane'
>>> jane.set_name("Jane Doe")
>>> jane.get_name()
'Jane Doe'
Here, you create an instance of Person 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 the Person class:
# person.py
class Person:
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:
>>> from person import Person
>>> 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 your Person 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
class Person:
def __init__(self, name):
self.name = name
@property
def name(self):
return self._name
@name.setter
def name(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:
>>> from person import Person
>>> 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.
Summarizing Class Syntax and Usage: A Complete Example
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 favorite code editor or IDE and create a file called employee.py. Then add the following code to it:
# employee.py
class Employee:
company = "Example, Inc."
def __init__(self, name, birth_date):
self.name = name
self.birth_date = birth_date
In this Employee 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 in ISO format to a datetime object:
# employee.py
from datetime import datetime
class Employee:
# ...
@property
def birth_date(self):
return self._birth_date
@birth_date.setter
def birth_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 a datetime.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
from datetime import datetime
class Employee:
# ...
def compute_age(self):
today = datetime.today()
age = today.year - self.birth_date.year
birthday = datetime(
today.year,
self.birth_date.month,
self.birth_date.day
)
if today < birthday:
age -= 1
return age
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 of Employee from dictionaries containing the data of your employees. You can add a convenient class method to quickly build objects that way:
# employee.py
from datetime import datetime
class Employee:
# ...
@classmethod
def from_dict(cls, data_dict):
return cls(**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 of Employee using the cls argument and unpacking the dictionary.
Finally, you’ll add suitable .__str__() and .__repr__() special methods to make your class friendly to users and developers, respectively:
# employee.py
from datetime import datetime
class Employee:
# ...
def __str__(self):
return f"{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