Big idea: A class's attributes are the data it holds, and its methods are the actions it can perform. Attributes come in two flavours: instance attributes (unique per object) and class attributes (shared by every object).
flowchart LR
CLS["📐 class Dog"] --> ATTR["🏷️ Attributes<br>(data)"]
CLS --> METH["🐝 Methods<br>(behavior)"]
ATTR --> CA["🏢 Class attributes<br>shared by all objects"]
ATTR --> IA["🏠 Instance attributes<br>unique per object"]
METH --> M1["def bark(self): ..."]
METH --> M2["def describe(self): ..."]
Attributes are variables that belong to a class or an object. They hold the data. There are two kinds:
| Instance attribute | Class attribute | |
|---|---|---|
| Belongs to | The object (each instance has its own copy) | The class itself (one copy shared by all instances) |
| Where it's defined | Usually in __init__ via self.name = ... |
Directly in the class body, above __init__ |
| Differs per object? | Yes — each object has its own value | No — same value for everyone |
| How to access | object.attr |
Class.attr or object.attr |
class Dog:
species = "French Bulldog" # 🏢 class attribute (shared)
def __init__(self, name):
self.name = name # 🏠 instance attribute (per-object)
print(Dog.species) # French Bulldog — read straight from the class
dog1 = Dog("Jack")
print(dog1.name) # Jack
print(dog1.species) # French Bulldog — inherited from the class
dog2 = Dog("Tom")
print(dog2.name) # Tom
print(dog2.species) # French BulldogYou can read class attributes directly from the class (Dog.species) — no object needed. But instance attributes only exist once you've created an object and __init__ has run — Dog.name would AttributeError.
A Car whose color and model differ per object:
class Car:
def __init__(self, color, model):
self.color = color # 🏠 instance attribute
self.model = model # 🏠 instance attribute
car_1 = Car("red", "Toyota Corolla")
car_2 = Car("green", "Lamborghini Revuelto")
print(car_1.model) # Toyota Corolla
print(car_2.model) # Lamborghini Revuelto
print(car_1.color) # red
print(car_2.color) # greenflowchart TD
A["dog1.species"] --> B{"Does dog1 have<br>its own 'species'?"}
B -- Yes --> C["Use the instance attribute"]
B -- No --> D{"Does Dog (class)<br>have 'species'?"}
D -- Yes --> E["Use the class attribute"]
D -- No --> F["AttributeError"]
Python looks at the instance first, then falls back to the class. That's why every Dog sees the same species even though only one copy exists.
Watch out: writing dog1.species = "Pug" does not change the class attribute — it creates a new instance attribute on dog1 that shadows the class attribute. Other dogs still see "French Bulldog". To actually change the shared value, write Dog.species = "Pug".
Methods are functions defined inside a class. They let an object perform actions — usually using its own attributes via self.
class Dog:
species = "French Bulldog"
def __init__(self, name):
self.name = name
def bark(self):
return f"{self.name} says woof woof!"
jack = Dog("Jack")
jill = Dog("Jill")
print(jack.bark()) # Jack says woof woof!
print(jill.bark()) # Jill says woof woof!- Defined with
definside the class. - First parameter is
self— the object the method is called on. - Called with dot notation + parentheses:
jack.bark(). - Same method, different output — because each object's
self.nameis different.
class Car:
def __init__(self, color, model):
self.color = color
self.model = model
def describe(self):
return f"This car is a {self.color} {self.model}"
car_1 = Car("red", "Toyota Corolla")
car_2 = Car("green", "Lamborghini Revuelto")
print(car_1.describe()) # This car is a red Toyota Corolla
print(car_2.describe()) # This car is a green Lamborghini Revuelto| Goal | Syntax | Example |
|---|---|---|
| Read an instance attribute | object.attribute |
car_1.color |
| Write an instance attribute | object.attribute = value |
car_1.color = "blue" |
| Read a class attribute | Class.attribute (or object.attribute) |
Dog.species |
| Change a class attribute (truly) | Class.attribute = value |
Dog.species = "Pug" |
| Call a method | object.method(args) |
car_1.describe() |
class Dog:
species = "French Bulldog" # 🏢 class attribute (shared)
count = 0 # 🏢 shared counter
def __init__(self, name):
self.name = name
Dog.count += 1 # bump the class-level counter
def bark(self):
return f"{self.name} ({Dog.species}) says woof! [#{Dog.count}]"
Dog("Jack")
Dog("Jill")
bo = Dog("Bo")
print(Dog.count) # 3 — same for every instance
print(bo.bark()) # Bo (French Bulldog) says woof! [#3]Why this works: instance attributes (self.name) are different for every dog, but the class attribute count is the one place that tracks “how many dogs exist?” — the perfect home for shared state.
- Initialize all instance attributes in
__init__so objects start in a consistent state. - Use class attributes for values that are the same for every instance (constants, shared counters, default config).
- Use instance attributes for anything that varies per object (
name,balance,email). - Access methods with dot notation + parentheses:
obj.method(). Forgetting()gives you the method object, not its result. - To change a shared class-level value, set it on the class (
Dog.species = ...), not on an instance. - Name attributes for what they hold; name methods for what they do.
- Confusing
dog1.species = "Pug"with changing the class attribute — it actually shadows it on that one instance. - Trying to read an instance attribute from the class:
Dog.name→AttributeError. - Forgetting
selfin a method definition, then gettingTypeError: ... takes 0 positional arguments but 1 was given. - Defining “instance attributes” at the class level by mistake — mutable defaults like
tricks = []are then shared by every dog, which surprises people. Useself.tricks = []inside__init__. - Calling methods like attributes (
dog.barkinstead ofdog.bark()) — you get the method object, not the result.
1. What are the two types of attributes in Python?
- Public and private attributes
- Local and global attributes
- Instance attributes and class attributes
- Mutable and immutable attributes
2. What is required to access instance attributes?
- The class name only
- Decorators
- An instance or object of the class
- A static method
Class attributes can be read straight from the class (Dog.species). Instance attributes only exist after you create an object and __init__ runs — so you need an actual object to read them.
3. How do you define and access methods?
- As standalone functions outside a class, accessed with brackets
- As variables in a class, accessed like attributes
- With special keywords, called automatically
- They are defined inside a class and accessed with dot notation
- Attributes are the data of a class; methods are its behavior.
- Instance attributes are unique to each object — set in
__init__viaself.attr = value. - Class attributes are shared by every object — defined directly in the class body.
- Python looks up
obj.attron the instance first, then on the class — that's how shared class attributes “appear” on every object. - Reassigning
obj.attr = ...creates an instance attribute that shadows the class one; changeClass.attr = ...to update the shared value. - Methods are functions inside a class with
selfas the first parameter — call them with dot notation + parentheses (obj.method()). - Use class attributes for shared constants/counters, and instance attributes for anything that varies per object.