Classes are a fundamental concept in Python. It is the basis of the standard library, the operation of most popular programs, and the language itself. If you want to become more than just a novice programmer, you must understand the essence and principle of working with classes and objects.

What are classes
This is the basic software component of OOP. In Python, classes are used to implement new types of objects and are created with a special class statement. Outwardly, they resemble standard built-in data types, such as numbers or sequences. But class objects have a significant difference - support for inheritance.
Object-oriented programming in Python is based entirely on hierarchical class inheritance. This is a universal way to adapt and reuse code. But an object-oriented approach is not mandatory. Python allows only procedural and functional programming without any problems.
The main purpose of classes in Python is to package data and executable code. Syntacticallythey are like def statements. Like functions, they create their own namespaces, which can be called repeatedly from any part of the program. Why then are they needed? Classes are a more powerful and versatile tool. Their potential is most revealed at the moment of creating new objects.

The importance of classes and the principle of inheritance
Each new object has its own namespace, which you can program, enter variables and create any functions. And there are also attributes inherited from the class: object.attribute. This is the essence of OOP.
Due to inheritance, a hierarchy tree is created. In practice, it looks like this. When the interpreter encounters an object.attribute expression, it looks for the first occurrence of attribute in the specified class. If no attribute is found, the interpreter continues searching through all related classes in the tree above, from left to right.
The search tree includes:
- superclasses that are at the very top of the hierarchy and implement common behavior;
- subclasses - below;
- instances are program elements with inherited behavior.

The figure shows the Python class tree. The example shows that Class 2 and 3 are superclasses. At the very bottom are two instances of Instance 1 and 2, in the middle is a subclass of Class 1. If you write the expression Instance2.w, it will cause the interpreter to look for the value of the.w attribute in the followingOK:
- Instance2;
- Class1;
- Class2;
- Class3.
The name.w, will be found in the superclass Class3. In OOP terminology, this means that Instance 2 "inherits" the.w attribute from Class3.
Note that the instances in the figure only inherit four attributes:.w,.x,.y, and.z:
- For Instance1.x and Instance2.x, the.x attribute will be found in Class 1, where the search will stop because Class 1 is lower in the tree than Class 2.
- For Instance1.y and Instance2.y, the.y attribute will be found in Class 1, where the search will stop because that's the only place it appears.
- For instances of Instance1.z and Instance2.z, the interpreter will find.z in Class 2 because it is located to the left of Class3 in the tree.
- For Instance2.name, the.name attribute will be found in Instance2 without searching through the tree.
The penultimate point is the most important. It demonstrates how Class 1 overrides the.x attribute, replacing the.x version of the superclass Class 2.
Objects, instances and methods
OOP operates with two main concepts: classes and objects. Classes create new types, and class objects in Python are instances of them. For example, all integer variables are of the built-in data type int. In OOP language, they are instances of the class int.
Classes are created with instructions, while objects are created with calls. They can store data and have their own functionality or class methods. Terminology plays an important role in Python. With its help, programmers distinguishindependent functions from those that belong to classes. Variables related to objects are called fields.
There are two kinds of fields in OOP. The first is the variables belonging to the whole class, the second is the variables of individual instances. Fields and methods together are class attributes. In Python, they are written in a code block after the class keyword.

Methods and value of self
Methods are functions with an additional name self. It is added to the beginning of the parameter list. If desired, the variable can be called by a different name, but such an initiative among programmers is not welcome. Self is a standard, easily recognizable name in the code. Moreover, some development environments are designed to work with it.
To better understand the meaning of self in OOP, imagine we have a class named ClassA with method methodA:
- >>>class ClassA;
- def methodA (self, argument1, argument2).
ObjectA is an instance of ClassA and the method call looks like this:
>>>objectA.methodA(argument1, argument2)
When the interpreter sees this line, it will automatically convert it as follows: ClassA.methodA(objectA, argument1, argument2). That is, the class instance uses the self variable as a reference to itself.

How to create variables, methods and class instances
We propose to analyze a practical example from the interactive Python shell. Create a class"Experiment One" begins with a compound instruction class:
- >>>class ExperimentFirst:
- def setinf(self, value): create the first method with arguments
- self.data=value
- def display(self): second method
- print(self.data) print instance data.
The obligatory indentation is followed by a block with nested def statements, in which two function objects are named setinf and display. They are used to create the ExperimentFirst.setinf and ExperimentFirst.display attributes. In fact, any name that is assigned a value at the top level in a nested block becomes an attribute.
To see how the methods work, you need to create two instances:
- >>>x=ExperimentFirst()Two instances are created;
- >>>y=ExperimentFirst()Each is a separate namespace.
Initially, instances do not store any information and are completely empty. But they are related to their class:
- >>>x.setinf("Learning Python") Calling a method where self is x.
- >>>y.setinf(3.14) Equivalent to ExperimentFirst.setinf(y, 3.14)
If we access the.setinf attribute of the ExperimentFirst class object through the name of instances x, y, then as a result of searching through the inheritance tree, the interpreter returns the value of the class attribute.
- >>>x.display() x and y have their own values self.data
- Learn Python
- >>>y.display()
- 3.14.

Operator overloading
In Python, classes can overload expression operators. This feature makes instances look like built-in data types. The process is to implement methods with special names that start and end with a double underscore.
Let's take a look at __init__ and __sub__ in action. The first method is called the class constructor. In Python, __init__ overloads the instantiation operation. The second __sub__ method implements the subtraction operation.
- >>>class Overload: create new class
- def __init__(self, start):
- self.data=start
- def __sub__(self, other):instance minus other
- return Overload(self.data - other) Result is a new instance
- >>>A=Overload(10) __init__(A, 10)
- >>>B=A – 2 __sub__(B, 2)
- >>>B.data B is a new instance of class Overload
- 8.
More about the __init__ method
The __init__ method is used most often when working with classes. It is indispensable for initializing various objects. __init__ does not need to be called separately. When a new instance is created, the method automatically receives the arguments specified in parentheses.
With the help of overload methods, you can implement any operations with built-in data types. Most are used only for special tasks that require objects to mimic the behavior of standard objects.
Methods are inherited fromsuperclasses and are optional. At the initial stages, you can easily do without them. But for a complete immersion in programming and the essence of OOP, you need the skill of working with operators.

Method __getitem__
The __getitem__ method performs an overload of element access by index. If it is inherited or present in the class definition, the interpreter will call it automatically on every indexing operation. For example, when an instance of F appears in an item-by-index expression such as F[i], the Python interpreter calls the __getitem__ method, passing the object F as the first argument and the index given in square brackets as the second.
The following class "Indexing Example" returns the square of the index value:
- >>>class Indexing Example:
- def __getitem__(self, index):
- return index2
- >>>F=ExampleIndexing()
- >>>F[2] F[i] expression calls F.__getitem__(i)
- 4
- >>>for i in range(5):
- print(F[i], end=" ")Calls __getitem__(F, i) every iteration
- 0 1 4 9 16
The same method can be used to extract a slice, which is often used when working with sequences. When processing lists, the standard syntax of the operation is as follows:
- >>>List=[13, 6, "i", "s", 74, 9]
- >>>List[2:4]
- [“and”, “with”]
- >>>List[1:]
- [6, "and", "with",74, 9]
- >>>List[:-1]
- [13, 6, "and", "with"]
- >>>List[::2]
- [13, "and", 74, 9]
Class that implements the __getitem__:
- >>>class Indexer:
- my_list=[13, 6, "and", "with", 74, 9]
- def __getitem__(self, index): Called when indexing or retrieving a slice
- print("getitem: ", index)
- return self.my_list[index] Indexing or extracting a slice
- >>>X=Indexer()
- >>>X[0] When indexing, __getitem__ gets an integer
- getitem: 0
- 13
- >>>X[2:4]When retrieving a slice, __getitem__ gets the slice object
- getitem: slice(2, 4, None)
- [“and”, “with”]

References to attributes
To get a reference to an attribute, use the special __getattr__ method. It is called with the attribute name as a string in cases where an attempt is made to obtain a reference to a non-existent or undefined attribute. When the interpreter can find the desired object in the inheritance tree, __getattr__.is not called.
The method is convenient for general processing of requests to attributes:
- >>>class Gone:
- def __getattr__(self, atname):
- if atname=="age":
- return 20
- else:
- raise AttributeError, atname
- >>>D=Gone()
- >>>D.age
- 20
- >>>D.name
- AttributeError: name
The Gone class and its instance D have no attributes of their own. Therefore, when accessing D.age, the __getattr__ method is automatically called. The instance itself is passed as self, and the name of the undefined "age" in the atname string. The class returns the result of calling the name D.age, despite the fact that it does not have this attribute.
If the class is not intended to handle the attribute, the __getattr__ method throws a built-in exception and thus informs the interpreter that the name is in fact undefined. In this case, an attempt to access D.name results in an error.
The __setattr__ operator overload method works in a similar way, intercepting each attempt to assign a value to an attribute. If this method is written in the class body, the expression "self.attribute=value" will be converted to a call to the self.__setattr_("attribute", value) method.
We have only described a few of the existing overload methods. The entire list is in the standard language manual and includes many more names.
Additional features
OOP is sometimes used for complex and non-standard tasks. Thanks to class inheritance in Python, the behavior of built-in data types and their capabilities are extensible and adaptable.
If you don't like the fact that indexing in sequences starts from zero, you can fix it with a class statement. To do this, you need to create a subclass of the list type with new names for all types and implement the necessary changes. Also in Python OOP there arefunction decorators, static methods, and many other tricky and special tricks.