A class is a blueprint for creating objects — it defines the data an object holds (attributes) and the behaviours it has (methods). Python’s object-oriented system is the foundation of everything in FastAPI: Pydantic’s BaseModel, SQLAlchemy’s Base, FastAPI’s HTTPException, and every database model you write are all classes. Understanding how to define classes, create instances, write methods, and use self correctly is the most important step up from procedural Python and the gateway to writing real web application code.
Defining a Class
# class keyword + PascalCase name (convention)
class Post:
# __init__ is the constructor — called when creating an instance
# 'self' is always the first parameter — refers to the instance being created
def __init__(self, title: str, body: str, published: bool = False):
# Instance attributes — stored on the object
self.title = title
self.body = body
self.published = published
self.view_count = 0 # default attribute not from parameter
# Instance method — first parameter is always self
def publish(self):
self.published = True
print(f"'{self.title}' is now published.")
def increment_views(self):
self.view_count += 1
def summary(self) -> str:
status = "published" if self.published else "draft"
return f"{self.title} ({status}) — {self.view_count} views"
# Create instances — call the class like a function
post1 = Post("Hello World", "My first post body")
post2 = Post("FastAPI Guide", "FastAPI tutorial body", published=True)
# Access attributes
print(post1.title) # Hello World
print(post1.published) # False
print(post2.published) # True
# Call methods
post1.publish() # 'Hello World' is now published.
post1.increment_views()
post1.increment_views()
print(post1.summary()) # Hello World (published) — 2 views
self is a convention, not a keyword — Python does not enforce the name self. However, using anything other than self is considered extremely bad practice and will confuse every Python developer who reads your code. The reason self must be the first parameter of every instance method is that Python automatically passes the calling instance as the first argument: post1.publish() is equivalent to Post.publish(post1).__init__, even if the initial value is None or a default. This makes the full structure of the class visible at a glance and prevents AttributeError from accessing attributes that were never set. Tools like mypy and IDE autocompletion rely on attributes being declared in __init__ to provide useful type checking and suggestions.tags = [] at the class level and one instance modifies it, all instances see the change. Always define mutable default attributes inside __init__ using self.tags = [], not at the class level. Read-only constants and class-wide configuration values are appropriate as class attributes.Class Attributes vs Instance Attributes
class Post:
# Class attribute — shared by ALL instances
post_count = 0
ALLOWED_STATUSES = {"draft", "published", "archived"}
def __init__(self, title: str):
# Instance attributes — unique to each instance
self.title = title
self.status = "draft"
Post.post_count += 1 # increment the class counter
@classmethod
def get_count(cls):
return cls.post_count
p1 = Post("First")
p2 = Post("Second")
print(Post.post_count) # 2
print(Post.get_count()) # 2
print(p1.post_count) # 2 — instance reads class attribute
print(p1.title) # "First" — instance attribute
print(p2.title) # "Second" — different for each instance
# Danger: mutating a mutable class attribute via instance
class Bad:
tags = [] # class-level list — shared!
b1 = Bad()
b2 = Bad()
b1.tags.append("python")
print(b2.tags) # ["python"] — b2's tags was also changed!
class Good:
def __init__(self):
self.tags = [] # instance-level list — independent ✓
Instance Methods in Depth
class User:
def __init__(self, name: str, email: str, role: str = "user"):
self.name = name
self.email = email.lower().strip()
self.role = role
self._password_hash = None # _ prefix = "private by convention"
def set_password(self, password: str) -> None:
import hashlib
self._password_hash = hashlib.sha256(password.encode()).hexdigest()
def check_password(self, password: str) -> bool:
import hashlib
hashed = hashlib.sha256(password.encode()).hexdigest()
return self._password_hash == hashed
def is_admin(self) -> bool:
return self.role == "admin"
def promote(self, new_role: str) -> None:
allowed = {"user", "editor", "admin"}
if new_role not in allowed:
raise ValueError(f"Invalid role: {new_role}")
self.role = new_role
print(f"{self.name} promoted to {new_role}")
def to_dict(self) -> dict:
"""Return a serialisable representation (no password)."""
return {"name": self.name, "email": self.email, "role": self.role}
# Using the class
alice = User("Alice Smith", " ALICE@EXAMPLE.COM ", "admin")
print(alice.email) # alice@example.com (normalised in __init__)
print(alice.is_admin()) # True
alice.set_password("secret123")
print(alice.check_password("secret123")) # True
print(alice.check_password("wrong")) # False
print(alice.to_dict())
# {"name": "Alice Smith", "email": "alice@example.com", "role": "admin"}
Python OOP vs JavaScript OOP
| Concept | Python | JavaScript |
|---|---|---|
| Define class | class MyClass: |
class MyClass {} |
| Constructor | def __init__(self, ...): |
constructor(...) {} |
| Instance reference | self (explicit param) |
this (implicit) |
| Private attribute | self._attr (convention) |
#attr (enforced) |
| Class attribute | MyClass.attr = x |
static attr = x |
| Inheritance | class Child(Parent): |
class Child extends Parent {} |
| Call parent | super().__init__(...) |
super(...) |
| Instantiate | obj = MyClass() |
obj = new MyClass() |
| Type check | isinstance(obj, MyClass) |
obj instanceof MyClass |
Common Mistakes
Mistake 1 — Forgetting self in method definitions
❌ Wrong — missing self parameter:
class Post:
def publish(): # missing self!
self.published = True # NameError: name 'self' is not defined
✅ Correct — always include self as first parameter:
class Post:
def publish(self): # ✓
self.published = True
Mistake 2 — Mutable class-level default
❌ Wrong — class-level list shared across instances:
class Post:
tags = [] # shared by all Post instances!
✅ Correct — initialise in __init__:
class Post:
def __init__(self):
self.tags = [] # each instance gets its own list ✓
Mistake 3 — Calling a method without an instance
❌ Wrong — calling instance method on the class:
Post.publish() # TypeError: publish() missing 1 required argument: 'self'
✅ Correct — call on an instance:
post = Post("Title", "Body")
post.publish() # ✓ Python passes post as self automatically
Quick Reference
| Task | Code |
|---|---|
| Define class | class MyClass: |
| Constructor | def __init__(self, params): self.attr = val |
| Instance method | def method(self): ... |
| Create instance | obj = MyClass(args) |
| Access attribute | obj.attr |
| Call method | obj.method() |
| Type check | isinstance(obj, MyClass) |
| Class attribute | MyClass.count = 0 (outside methods) |
| Private convention | self._private_attr |