Overview
This article provides an easier-to-understand summary of all the content in section 9. Classes of the official Python v3.13 Tutorial.
1. Basic Concepts of Classes
- Classes provide a means of bundling data and functionality together.
- Creating a new class creates a new type of object.
- Objects created from that class are called instances. To use an analogy, a class is like a fish-shaped pastry mold, and the instances are the fish-shaped pastries made from it.
2. Characteristics of Python Classes
- Python classes are a mix of mechanisms from C++ and Modula-3, designed with minimal syntax.
- Python classes satisfy all the standard features of Object-Oriented Programming.
- They can inherit attributes and methods from multiple parent classes.
- A derived class can override any method of its base class(es) and can also call the method of the base class with the same name.
- In C++ terminology, all members (attributes and methods) of a Python class are public (with some exceptions), and all member functions are virtual.
- Python does not have keywords like
privateorvirtual. - "Public" members mean that class attributes and methods can be accessed from outside the class anywhere.
- "Virtual" member functions mean that a derived class can redefine any method of its base class.
- The class itself is also an object, so it can be imported and renamed.
- Unlike C++ and Modula-3, built-in types (like
int,string, etc.) can be used as base classes for inheritance.
- Like in C++, built-in operators (+, -, /, %, etc.) can be redefined for class instances.
3. Scopes and Namespaces
This section can be a bit confusing and complex, but it's crucial for understanding the backbone of how Python classes work. Skipping this part will make it difficult to understand the following sections.
3.1. Names and Objects
Different names can be bound to the same object. This is known as aliasing in other languages. In Python, it's often not immediately obvious and can be safely ignored when dealing with immutable basic types (numbers, strings, tuples). However, aliasing has significant consequences when dealing with mutable objects (lists, dictionaries, etc.). Aliases behave somewhat like pointers and can be useful. For example, passing an object is cheap since only a pointer is passed, and if a function modifies an object, you don't need two different arguments.
3.2. Namespace
A namespace is a mapping from names to objects. Most namespaces are currently implemented as Python dictionaries.
Examples of namespaces are:
- The set of built-in names: containing functions like
abs().
- The global names in a module.
- The local names in a function invocation.
Also, the attributes of an object form a namespace.
One important thing to know about namespaces is that there is absolutely no relation between names in different namespaces. For instance, two different modules may both define a function
maximize without confusion.Namespaces have different lifetimes:
- The namespace containing the built-in names is created when the Python interpreter starts up, and is never deleted.
- The global namespace for a module is created when the module definition is read in (e.g.,
import module); normally, module namespaces also last until the interpreter quits.
- The local namespace for a function is created when the function is called, and deleted when the function returns or raises an exception. Of course, recursive invocations each have their own local namespace.
3.3. Attribute
An attribute is any name following a dot. For example, in the expression
z.real, real is an attribute of the object z. When you refer to a name in a module, you are referring to an attribute of that module. In modname.funcname, modname is a module object and funcname is an attribute of it. There is a straightforward mapping between the module’s global names and the attributes of the module object.Attributes may be read-only or writable. If writable, assignment to attributes is possible. Module attributes are writable: you can write
modname.the_answer = 42. You can also delete attributes from the modname object with del modname.the_answer.3.4. Scope
A scope is a textual region of a Python program where a namespace is directly accessible. “Directly accessible” here means that an unqualified reference to a name (like
x) attempts to find the name in the namespace.Although scopes are determined statically, they are used dynamically. At any time during execution, there are at least three or four nested scopes whose namespaces are directly accessible:
- The innermost scope, which is searched first, contains the local names.
- The scopes of any enclosing functions, which are searched starting with the nearest enclosing scope, contain non-local, but also non-global names.
- The next-to-last scope contains the current module’s global names.
- The outermost scope (searched last) is the namespace containing built-in names.
If a name is declared
global, then all references and assignments go directly to the next-to-last scope.If a name is declared
nonlocal, the variable is bound to the innermost scope. If not declared nonlocal, assignments to variables in an enclosing scope are read-only. If you try to write to it, a new local variable is created in the innermost scope, leaving the outer variable unchanged.Normally, the local scope references the local names of the (textually) current function. Outside functions, the local scope references the same namespace as the global scope. Class definitions place yet another namespace in the local scope.
It is important to realize that scopes are determined textually. The global scope of a function defined in a module is that module’s namespace, no matter from where or by what alias the function is called. Thus, scopes are determined statically, but they are searched dynamically at run time.
A special quirk of Python is that – if no
global or nonlocal statement is in effect – assignments to names always go into the innermost scope. Assignments do not copy data — they just bind names to objects. The del statement also unbinds names by referencing the local scope. In fact, all operations that introduce new names use the local scope: in particular, import statements and function definitions bind the module or function name in the local scope.3.5. Example
In the example below, even after calling
do_local(), the spam inside do_local is in the local scope, and the spam in scope_test is in the enclosing scope, so the print statement shows the unchanged value. After do_nonlocal(), the spam inside do_nonlocal is moved to the enclosing scope, so the printed value changes. After do_global, spam is moved to the global scope, so you have to read it outside the function to see the changed value.def scope_test(): def do_local(): spam = "local spam" def do_nonlocal(): nonlocal spam spam = "nonlocal spam" def do_global(): global spam spam = "global spam" spam = "test spam" do_local() print("After local assignment:", spam) do_nonlocal() print("After nonlocal assignment:", spam) do_global() print("After global assignment:", spam) scope_test() print("In global scope:", spam)
After local assignment: test spam After nonlocal assignment: nonlocal spam After global assignment: nonlocal spam In global scope: global spam
4. How Classes Work
4.1. Class Definition
class ClassName: <statement-1> . . . <statement-N>
Like function definitions, class definitions must be executed before they have any effect. You can define a class with the
class statement. The statements inside a class are usually function definitions, but other statements are allowed. When a class definition is entered, a new namespace is created, and used as the local scope.When a class definition is left normally, a class object is created.
4.2. Class Objects
Class objects support two kinds of operations: attribute references and instantiation.
4.2.1. Attribute References
Attribute references use the standard syntax used for all attribute references in Python:
obj.name. All valid attribute names in the class object's namespace when it was created are referenceable.class MyClass: """A simple example class""" i = 12345 def f(self): return 'hello world'
MyClass.i returns an integer object, and MyClass.f returns a function object. Each attribute is assignable, so you can change its value. MyClass.__doc__ is also an attribute, returning "A simple example class".4.2.2. Instantiation
The instantiation operation (calling a class object) creates an empty object. It's written like a function call. The class object is just a function that returns a new instance and has no parameters.
x = MyClass()
A class can customize its initial state with a special method called
__init__(). In this case, arguments from the instantiation are passed to __init__().class Complex: def __init__(self, realpart, imagpart): self.r = realpart self.i = imagpart x = Complex(3.0, -4.5) x.r, x.i
(3.0, -4.5)
4.3. Instance Objects
The only operations understood by instance objects are attribute references. There are two kinds of valid attribute names: data attributes and methods.
Data attributes correspond to “data members” in C++. Data attributes need not be declared like local variables; they spring into existence when they are first assigned to. For example, if
x is the instance of MyClass created above, the following piece of code will print the value 16, without leaving a trace, even though counter was not defined in MyClass:x.counter = 1 while x.counter < 10: x.counter = x.counter * 2 print(x.counter) del x.counter
A method is a function that “belongs to” an object. It is an instance attribute reference.
Valid method names depend on the class. All function objects that are class attributes define corresponding methods of its instances. So in our
MyClass example, MyClass.f is a function object, so x.f is a valid method reference. MyClass.i is not, since it is an integer object. Meanwhile, x.f is a method object and MyClass.f is a function object, so they are not the same.4.4. Method Objects
You can call a method as follows. But the
f function in MyClass requires an argument self. How does the code below work without providing an argument?x.f()
xf = x.f while True: print(xf())
The reason is as follows. The special thing about methods is that the instance object is passed as the first argument of the function. That is,
x.f() is exactly equivalent to MyClass.f(x). In general, calling a method with a list of n arguments is equivalent to calling the corresponding function with an argument list that is created by inserting the instance object before the first argument.Overall, methods work as follows:
- When a non-data attribute of an instance is referenced, the instance’s class is searched.
- If the name denotes a valid class attribute that is a function object, a method object is created by packing (pointers to) the instance object and the function object just found together in an abstract object.
- When the method object is called with an argument list, a new argument list is constructed from the instance object and the argument list, and the function object is called with this new argument list.
4.5. Class and Instance Variables
Generally speaking, class variables are for attributes and methods shared by all instances of a class, and instance variables are for data unique to each instance.
class Dog: kind = 'canine' # class variable shared by all instances def __init__(self, name): self.name = name # instance variable unique to each instance >>> d = Dog('Fido') >>> e = Dog('Buddy') >>> d.kind # shared by all dogs 'canine' >>> e.kind # shared by all dogs 'canine' >>> d.name # unique to d 'Fido' >>> e.name # unique to e 'Buddy'
As discussed in Names and Objects, shared data can have possibly surprising effects with involving mutable objects.
For example, the
tricks list, when used as a class variable, is shared by all Dog instances, which can lead to unintended results.class Dog: tricks = [] # mistaken use of a class variable def __init__(self, name): self.name = name def add_trick(self, trick): self.tricks.append(trick) >>> d = Dog('Fido') >>> e = Dog('Buddy') >>> d.add_trick('roll over') >>> e.add_trick('play dead') >>> d.tricks # unexpectedly shared by all dogs ['roll over', 'play dead']
The correct way is to use an instance variable as shown below.
class Dog: def __init__(self, name): self.name = name self.tricks = [] # creates a new empty list for each dog def add_trick(self, trick): self.tricks.append(trick) >>> d = Dog('Fido') >>> e = Dog('Buddy') >>> d.add_trick('roll over') >>> e.add_trick('play dead') >>> d.tricks ['roll over'] >>> e.tricks ['play dead']
5. Other Useful Information
class Warehouse: purpose = 'storage' region = 'west' w1 = Warehouse() print(w1.purpose, w1.region) # -> storage west w2 = Warehouse() w2.region = 'east' print(w2.purpose, w2.region) # -> storage east
- Data attributes may be referenced by methods as well as by ordinary users (clients) of an object. In other words, classes are not usable to implement pure abstract data types. In fact, nothing in Python makes it possible to enforce data hiding — it is all based upon convention. On the other hand, the Python implementation, written in C, can completely hide implementation details and control access to an object if necessary.
- Clients should use data attributes with care — clients may mess up invariants maintained by the methods by stamping on their data attributes. So, a naming convention can solve this headache very well.
- There is no shorthand for referencing data attributes from within methods. It always has to be referenced as
self.attr. This reduces the chance of confusion between local variables and instance variables.
- The first argument of a method is often called
self. In fact, the nameselfhas absolutely no special meaning to Python. However, not following the convention will make your code less readable to other Python programmers, and it is also conceivable that a class browser program might be written that relies upon such a convention.
- Any function object that is a class attribute defines a method for instances of that class. It is not necessary that the function definition is textually enclosed in the class definition. You can assign a function object to a local variable, as in the example below.
# Function defined outside the class def f1(self, x, y): return min(x, x+y) class C: f = f1 def g(self): return 'hello world' h = g
In the example above,
f, g, and h are all attributes of class C that refer to function objects, and consequently they are all methods of instances of C. h is exactly equivalent to g.- Methods may call other methods by using method attributes of the
selfargument.
class Bag: def __init__(self): self.data = [] def add(self, x): self.data.append(x) def addtwice(self, x): self.add(x) self.add(x)
- Methods may reference global names in the same way as ordinary functions. The global scope associated with a method is the module containing its definition. (A class is never used as a global scope.) While one rarely encounters a good reason for using global data in a method, there are many legitimate uses of the global scope: for one thing, functions and modules imported into the global scope can be used by methods.
- Each value is an object, and therefore has a class. It is stored in
object.__class__.
6. Inheritance
6.1. Definition
Of course, a language feature would not be worthy of the name “class” without supporting inheritance. The syntax for a derived class definition looks like this:
class DerivedClassName(BaseClassName): <statement-1> . . . <statement-N>
The name
BaseClassName must be defined in a scope containing the derived class definition. In place of a base class name, other arbitrary expressions are also allowed. This can be useful, for example, when the base class is defined in another module:class DerivedClassName(modname.BaseClassName):
Execution of a derived class definition proceeds the same as for a base class. When the class object is constructed, the base class is remembered. If a requested attribute or method is not found in the derived class, the search proceeds to look in the base class. This rule is applied recursively if the base class itself is derived from some other class.
There’s nothing special about instantiation of derived classes.
6.2. Method Override
A derived class may override methods of its base classes. Because methods have no special privileges when calling other methods of the same object, a method of a base class that calls another method defined in the same base class may end up calling a method of a derived class that overrides it.
This is complex, so let's look at the example below. In
apple.introduce(), the apple instance has no introduce method, so it looks in the base class Food. It calls another method get_taste, and since the self object is apple, it is overridden to "sweet". (For C++ programmers: all methods in Python are effectively virtual.)class Food: def __init__(self, name): self.name = name def introduce(self): print(f"I am {self.get_taste()} {self.name}!") def get_taste(self): return "salty" class Apple(Food): def get_taste(self): return "sweet" pizza = Food("pizza") pizza.introduce() # -> I am salty pizza! apple = Apple("apple") apple.introduce() # -> I am sweet apple!
An overriding method in a derived class may in fact want to extend rather than simply replace the base class method of the same name. A simple way to call the base class method directly is to call
BaseClassName.methodname(self, arguments). (This is occasionally useful to clients as well, provided that BaseClassName is in the global scope.)In the example above, you can change
print(f"I am {self.get_taste()} {self.name}!") to print(f"I am {Food.get_taste(self)} {self.name}!").6.3. Built-in functions
isinstance(): Used to check an instance’s type. isinstance(obj, int) will be True only if obj.__class__ is int or some class derived from int.issubclass(): Used to check class inheritance. issubclass(bool, int) is True since bool is a subclass of int. However, issubclass(float, int) is False since float is not a subclass of int.6.4. Multiple Inheritance
Python supports a form of multiple inheritance as well. A class definition with multiple base classes looks like this:
class DerivedClassName(Base1, Base2, Base3): <statement-1> . . . <statement-N>
For most purposes, in the simplest cases, you can think of the search for attributes inherited from a parent class as depth-first, left-to-right, not searching twice in the same class where there is an overlap in the hierarchy. Thus, if an attribute is not found in
DerivedClassName, it is searched for in Base1, then (recursively) in the base classes of Base1, and if it was not found there, it was searched for in Base2, and so on.In fact, it is slightly more complex than that; the method resolution order (MRO) changes dynamically to support cooperative calls to
super(). In multiple inheritance, a diamond relationship can occur where a class at the bottom inherits from all classes above it. To prevent the top-most base class from being accessed more than once, the order is linearized. Thus, super() does not call the parent class, but the next class in the MRO. For more details, see the link below.6 --- Level 3 | O | (more general) / --- \ / | \ | / | \ | / | \ | --- --- --- | Level 2 3 | D | 4| E | | F | 5 | --- --- --- | \ \ _ / | | \ / \ _ | | \ / \ | | --- --- | Level 1 1 | B | | C | 2 | --- --- | \ / | \ / \ / --- Level 0 0 | A | (more specialized) ---
7. Private Variables
“Private” instance variables that cannot be accessed except from inside an object don’t exist in Python. However, there is a convention that is followed by most Python code: a name prefixed with an underscore (e.g.
_spam) should be treated as a non-public part of the API (whether it is a function, a method or a data member).On the other hand, Python provides limited support for a mechanism called name mangling. It is used to avoid name clashes between base and subclasses. Any identifier of the form
__spam (at least two leading underscores) that exists anywhere in a class definition is textually replaced with _classname__spam, where classname is the current class name.The example below shows how name mangling allows a subclass to override a method without breaking the parent class.
class Mapping: def __init__(self, iterable): self.items_list = [] self.__update(iterable) ## name mangling def update(self, iterable): for item in iterable: self.items_list.append(item) __update = update # private copy of original update() method class MappingSubclass(Mapping): def update(self, keys, values): # provides new signature for update() # but does not break __init__() for item in zip(keys, values): self.items_list.append(item)
If
MappingSubclass were to use __update, it would be replaced with _MappingSubclass__update in MappingSubclass and _Mapping__update in the Mapping class, so no collision would occur.Note that this is designed to avoid collisions; it is still possible to access or modify a variable that is considered private.
Also note that in places like
exec(), eval(), global, getattr(), setattr(), and delattr(), there is no context to know the classname, so you must pass the mangled identifier value directly.8. Odds and Ends
- Sometimes it is useful to have a data type similar to the C “struct”, which can be done with dataclasses as shown below.
from dataclasses import dataclass @dataclass class Employee: name: str dept: str salary: int
john = Employee('john', 'computer lab', 1000) john.dept # -> 'computer lab' john.salary # -> 1000
- In code that expects a specific data type, you can pass a class that emulates the methods of that type instead. For example, if you have a function that accepts a file object, you can define a class with
read()andreadline()methods and pass it a string buffer instead.
- Instance method objects have attributes as well:
m.__self__is the instance object with the methodm(), andm.__func__is the function object corresponding to the method.
9. Iterators
Most container objects can be looped over using a
for statement:for element in [1, 2, 3]: print(element) for element in (1, 2, 3): print(element) for key in {'one':1, 'two':2}: print(key) for char in "123": print(char) for line in open("myfile.txt"): print(line, end='')
Behind the scenes, the
for statement calls iter() on the container object. The function returns an iterator object that defines the method __next__() which accesses elements in the container one at a time. When there are no more elements, __next__() raises a StopIteration exception. You can call the __next__() method using the next() built-in function.s = 'abc' it = iter(s) it # -> <str_iterator object at 0x10c90e650> next(it) # -> 'a' next(it) # -> 'b' next(it) # -> 'c' next(it) # -> Traceback (most recent call last): # -> File "<stdin>", line 1, in <module> # -> next(it) # -> StopIteration
Having seen the mechanics of the iterator protocol, it is easy to add iterator behavior to your own classes. Define an
__iter__() method which returns an object with a __next__() method. If the class defines __next__(), then __iter__() can just return self:class Reverse: """Iterator for looping over a sequence backwards.""" def __init__(self, data): self.data = data self.index = len(data) def __iter__(self): return self def __next__(self): if self.index == 0: raise StopIteration self.index = self.index - 1 return self.data[self.index]
rev = Reverse('spam') iter(rev) # -> <__main__.Reverse object at 0x00A1DB50> for char in rev: print(char) # -> m # -> a # -> p # -> s
10. Generators
Generators are a simple and powerful tool for creating iterators. They are written like regular functions but use the
yield statement whenever they want to return data. Each time next() is called on it, the generator resumes where it left off (it remembers all the data values and which statement was last executed).def reverse(data): for index in range(len(data)-1, -1, -1): yield data[index] for char in reverse('golf'): print(char)
f l o g
Anything that can be done with generators can also be done with class-based iterators as described in the previous section.
What makes generators so compact is that the
__iter__() and __next__() methods are created automatically.- Local variables and execution state are automatically saved between calls, so you don't need to use
self.dataandself.index.
- Generators automatically raise
StopIterationwhen they terminate.
11. Generator Expressions
Some simple generators can be coded succinctly as expressions using a syntax similar to list comprehensions. These expressions are designed for situations where the generator is used right away by an enclosing function. Generator expressions are more compact but less versatile than full generator definitions and tend to be more memory friendly than equivalent list comprehensions.
sum(i*i for i in range(10)) # sum of squares # -> 285 xvec = [10, 20, 30] yvec = [7, 5, 3] sum(x*y for x,y in zip(xvec, yvec)) # dot product # -> 260 unique_words = set(word for line in page for word in line.split()) valedictorian = max((student.gpa, student.name) for student in graduates) data = 'golf' list(data[i] for i in range(len(data)-1, -1, -1)) # range(start, stop, step) # -> ['f', 'l', 'o', 'g']
As in
set(word for line in page for word in line.split()), the value you ultimately want to obtain comes first, followed by the loops from outermost to innermost.Note that list comprehensions use square brackets, while generators use parentheses.
squares_list = [i*i for i in range(10)]
Reference
[1] Python Software Foundation. "9. Classes." The Python Tutorial, version 3.13, Python Software Foundation, 2024, docs.python.org/3.13/tutorial/controlflow.html#special-parameters. Accessed 20 July 2025.
