Skip to content
wannacode

What all objects share: class attributes

Problem

Every product is #1

The shop wants every new product to get the next number: 1, 2, 3… You put a counter _next_id in the product. __init__ gives the product the current number and increases the counter:

class Product:
def __init__(self, name):
self._next_id = 1
self._name = name
self._id = self._next_id
self._next_id += 1
def __str__(self):
return f"#{self._id} {self._name}"
bread = Product("Bread")
milk = Product("Milk")
coffee = Product("Coffee")
print(bread)
print(milk)
print(coffee)
Output
#1 Bread
#1 Milk
#1 Coffee

They are all #1. The reason is that _next_id is an attribute of the object, just like the name. Every new product gets its own counter, which starts at 1. The bread's counter never reaches the milk.

The idea

An attribute of the class, not of the object

The counter is not part of one product. It is shared by all products. Write it in the class itself, outside __init__:

class Product:
_next_id = 1

An attribute written in the class is a class attribute. There is one, no matter how many objects you create, and all the objects share it. In __init__, you reach it through the class name: Product._next_id.

shop.py
class Product:
_next_id = 1
def __init__(self, name):
self._name = name
self._id = Product._next_id
Product._next_id += 1
def __str__(self):
return f"#{self._id} {self._name}"
bread = Product("Bread")
milk = Product("Milk")
coffee = Product("Coffee")
print(bread)
print(milk)
print(coffee)
Output
#1 Bread
#2 Milk
#3 Coffee

The bread takes 1 and makes the counter 2. The milk sees the same counter, takes 2 and makes it 3.

class Product:
_next_id = 1
def __init__(self, name):
self._name = name
self._id = Product._next_id
Product._next_id += 1
bread = Product("Bread")
milk = Product("Milk")
coffee = Product("Coffee")
Step by step, in words
  1. _next_id is a class attribute: there is one, not one in each object.
  2. Product("Bread"): a new object. __init__ stores the name.
  3. self._id = Product._next_id: the object takes the current number, 1.
  4. Product._next_id += 1 changes the shared counter: now it is 2.
  5. Milk: the same counter. It takes 2 and makes it 3.
  6. Coffee: it takes 3 and makes it 4.
  7. There is one counter for all of them, so the numbers go in order.
More precisely, if you are curious

Be careful with self. Reading self._next_id finds the class attribute, but writing self._next_id += 1 makes a new attribute on the object, and the shared counter does not move:

class Product:
_next_id = 1
def __init__(self, name):
self._name = name
self._id = self._next_id
self._next_id += 1
def __str__(self):
return f"#{self._id} {self._name}"
bread = Product("Bread")
milk = Product("Milk")
print(bread)
print(milk)
print(Product._next_id)
Output
#1 Bread
#1 Milk
1

That is why you change the shared value only through the class name: Product._next_id += 1.

Try it

Shared and own

What will the program print?

class Order:
count = 0
def __init__(self):
Order.count += 1
self.items = 0
self.items += 1
a = Order()
b = Order()
c = Order()
print(Order.count, c.items)
Answer

The idea

@staticmethod: work without an object

The same goes for methods. A method with @staticmethod above it is a method of the class. It has no self, and you call it through the class name without making an object:

class Calculator:
@staticmethod
def add(a, b):
return a + b
@staticmethod
def average(values):
total = 0
for v in values:
total += v
return total / len(values)
print(Calculator.add(10, 20))
grades = [5, 6, 4, 6]
print(Calculator.average(grades))
print(max(3, 8))
Output
30
5.25
8

Calculator.add does not need an object: everything it needs comes as parameters. In Python, work like this is often just a function outside the class, like max and math.sqrt. You put it in a class when it is closely tied to the class and you want it under the class name.

Python has no main in a class for the program to start from: the program is the lines of the file, from top to bottom. That is why the functions from the "Functions" module did not need a class.

More precisely, if you are curious

There is also @classmethod. It gets the class itself, cls, as its first parameter, and it reaches the class attributes without an object:

class Product:
_next_id = 1
def __init__(self, name):
self._name = name
self._id = Product._next_id
Product._next_id += 1
@classmethod
def created(cls):
return cls._next_id - 1
bread = Product("Bread")
milk = Product("Milk")
print(Product.created())
Output
2

@staticmethod gets nothing, @classmethod gets the class, and an ordinary method gets the object (self).

Try it

Find the bug

tea.label() stops the program. Which line has the bug?

Pick the line with the bug.

The idea

Constants

VAT is 20% for all products, and it does not change while the program runs. A value like that is a constant:

class Prices:
VAT_RATE = 0.20
@staticmethod
def with_vat(price):
return price * (1 + Prices.VAT_RATE)
print(Prices.with_vat(50))
print(f"VAT: {Prices.VAT_RATE * 100}%")
Output
60.0
VAT: 20.0%

Python has no constants. There is a convention: a name in capital letters with underscores, VAT_RATE, means "this does not change". Written in the class, it is a class attribute.

If someone tries to change it,

Prices.VAT_RATE = 0.25

nothing protects it, and Prices.VAT_RATE is now:

Output
0.25

What the convention gives you is that the number 0.20 is written in one place with a clear name. The capital letters tell everyone who reads the code "do not touch", but protecting the value is up to you.

More precisely, if you are curious

Final from the typing module looks like a constant, but Python does not check it. Only the editor or a tool like mypy warns you:

from typing import Final
class Prices:
VAT_RATE: Final = 0.20
Prices.VAT_RATE = 0.25
print(Prices.VAT_RATE)
Output
0.25

Sometimes the value is different for each object, but it must not change after the object is created. The product number is like that. Make it a read-only property, @property without a setter (the "Encapsulation" lesson):

class Product:
_next_id = 1
def __init__(self, name):
self._name = name
self._id = Product._next_id
Product._next_id += 1
@property
def id(self):
return self._id

coffee.id reads the number, and coffee.id = 5 stops with an AttributeError, because the property has no setter.

Try it

What is missing?

A year has 12 months, and that will not change. What name goes in the blank?
class Calendar:
     = 12

The idea

When on the class and when on the object

Ask yourself: "Does this belong to a specific object?"

  • Yes (the balance of the account, the name of the product, withdrawing from an account): an attribute of the object in __init__, or a method with self.
  • No (the number of all accounts, VAT, the sum of two numbers): a class attribute, @staticmethod or @classmethod.

A common mistake is a list as a class attribute. There is one list for all objects, so what you put in one cart shows up in the other:

class Cart:
items = []
def add(self, name):
self.items.append(name)
first = Cart()
second = Cart()
first.add("Bread")
print(second.items)
Output
['Bread']

The list belongs to each cart, so its place is in __init__: self.items = []. Otherwise all the "objects" share the same data, and you are back at the problem with the parallel lists from the first lesson.

Exercise

What all objects share

Solve the tasks in your editor.

1. Orders. An Order class with a customer and an amount. Every new order gets the next number from a counter that is a class attribute. A method count() with @classmethod returns how many orders have been created.

The program
orders = [
Order("Maria", 45.5),
Order("Peter", 120),
Order("Elena", 18.9),
]
for o in orders:
print(o)
print(f"Orders: {Order.count()}")
Output
Order #1: Maria, €45.5
Order #2: Peter, €120
Order #3: Elena, €18.9
Orders: 3

2. Calculator. A Calculator class with methods with @staticmethod for adding, subtracting and multiplying whole numbers, and for dividing decimal numbers.

The program
print(Calculator.add(10, 20))
print(Calculator.subtract(10, 15))
print(Calculator.multiply(6, 7))
print(Calculator.divide(5, 2))
Output
30
-5
42
2.5

3. Exchange. A Converter class with constants for the rate: €1 = 1.08 US dollars and €1 = 0.85 British pounds (the rates are examples). Methods with @staticmethod convert an amount in euros to dollars and to pounds. For €20:

Output
20 EUR = 21.6 USD
20 EUR = 17.0 GBP

More tasks

  • Numbered accounts. Add to the Account from the "Encapsulation" lesson an account number that comes from a counter that is a class attribute, and a read-only property for the number.
  • Exchange both ways. Add methods to Converter that convert from dollars and from pounds to euros. Convert €20 to dollars and back to euros. Do you get exactly 20?
  • A limit. The shop accepts at most 3 products in a cart. Add a constant for that, and make the add method refuse the fourth one.

In short

The module in short

  • A class is your own type, and an object is a specific instance of it, created with Product(). The variable points to the object. Your own type
  • __init__ makes sure the object is created whole. self is the current object. Constructors and self
  • Instance methods work with the object's attributes. __str__ says what it looks like as text. Behavior
  • _name says "internal", and @property with a setter lets changes through only after a check. Encapsulation
  • Class attributes, @staticmethod and @classmethod are what the class shares: counters, helper methods and constants.

Data and the rules for it in one place, with a clear mark on what is internal: that is object-oriented programming. In the next module, classes will start to share code with each other.

Test

Check yourself

1. You create 5 objects from a class with the class attribute count = 0. How many copies of count are in memory?
2. What can a @staticmethod NOT do?
3. What does a @classmethod get as its first parameter?
4. The class has _next_id = 1. What does self._next_id += 1 do in __init__?
5. What happens with Prices.VAT_RATE = 0.25?
6. What is a good candidate for a @staticmethod?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·