Object-Oriented and Advanced Python
Structure larger programs with classes, iterators, decorators, and typing.
Content
Classes and Objects
Versions:
Watch & Learn
AI-discovered learning video
Sign in to watch the learning video for this topic.
Classes and Objects in Python — OOP for CS50 Students
"If functions are verbs, classes are full sentences with nouns, adjectives, and a little drama."
You've just moved from writing readable, expressive Python (thanks PEP 8!) and learned to manage projects with venvs and packaging. Now we level up: turn your ideas into objects that behave like real things in code. This is where readability meets structure and reusability — and where bugs become slightly more dramatic.
Why classes matter (and when to use them)
- What they are: Classes are blueprints; objects are instances of those blueprints. Classes bundle state (data) and behavior (methods) together.
- Why they matter: They make complex programs manageable by modeling real-world things, enforcing invariants, and enabling reuse through inheritance and composition.
- Where they show up: GUIs (widgets), web frameworks (request/response objects), games (entities), data models (ORMs), and basically any medium-sized Python project that isn't just a one-off script.
Quick contextual tie-in: your previous work with packaging means you'll soon package class-based modules; PEP 8 tells you class names use CamelCase; venvs are where you'll test these classes safely.
Anatomy of a class: the basics
class Person:
"""A simple Person class."""
species = 'Homo sapiens' # class variable: shared across instances
def __init__(self, name, age):
self.name = name # instance variable: unique per instance
self.age = age
def greet(self):
return f"Hi, I'm {self.name} and I'm {self.age}."
def __repr__(self):
return f"Person(name={self.name!r}, age={self.age!r})"
Micro explanations
- init initializes the instance — think of it as setup. It's not a constructor in the Java sense, but close enough.
- self is the instance itself. Forgetting it is the #1 newbie crime (Python will scream at you).
- class vs instance variables:
speciesabove is shared;nameandageare per instance. - repr helps debugging; make it unambiguous. Implement str for user-facing strings.
Encapsulation: keep the internals honest
Encapsulation is about controlling access and invariants.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance # '_' signals "internal use"
def deposit(self, amount):
if amount <= 0:
raise ValueError("deposit must be positive")
self._balance += amount
def withdraw(self, amount):
if amount > self._balance:
raise ValueError("insufficient funds")
self._balance -= amount
@property
def balance(self):
return self._balance
- Use
_privatenaming to indicate non-public attributes. - Use @property to expose read-only or computed attributes without changing the API.
Inheritance and polymorphism: reuse and specialize
Inheritance lets a class build on another. Polymorphism means different classes share the same interface.
class Student(Person):
def __init__(self, name, age, student_id):
super().__init__(name, age)
self.student_id = student_id
def greet(self): # override
return f"Hi, I'm {self.name}, student #{self.student_id}."
Use inheritance when there's a clear "is-a" relationship (Student is-a Person). Prefer composition when behavior is best expressed by "has-a" (Car has-an Engine).
Composition: build complexity from parts
class Engine:
def __init__(self, hp):
self.hp = hp
class Car:
def __init__(self, make, engine: Engine):
self.make = make
self.engine = engine
Composition is often safer than inheritance and keeps code flexible.
Advanced-ish Python: dataclasses, classmethods, and dunder magic
- dataclasses (Python 3.7+): boilerplate reduction for classes that mostly hold data.
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
- classmethod: alternate constructors or operations related to the class, not instances.
class Person:
@classmethod
def from_birth_year(cls, name, year):
import datetime
return cls(name, datetime.date.today().year - year)
- staticmethod: utility functions that live in the class namespace but don't touch class/instance data.
- Implement eq, lt if you want instances to compare sensibly.
Common pitfalls and "why do people keep misunderstanding this?"
- Mutating class-level mutable defaults:
class Bad:
items = [] # shared list for all instances — usually a bug
- Forgetting self in method signatures — Python will complain loudly.
- Using inheritance because it's "cool" instead of the right modeling choice.
- Overcomplicating: not every problem needs classes. Functions and modules are fine.
Small checklist before you ship a class
- Follow PEP 8: ClassName (CamelCase), method_and_variable (snake_case).
- Keep methods short and single-purpose.
- Use properties to protect invariants.
- Write repr for debugging, str for users.
- Prefer composition over deep inheritance trees.
- Add unit tests (in your venv!) that exercise behavior, edge cases, and invariants.
Quick comparison table (conceptual)
- Class — blueprint
- Object/Instance — actual thing
- Method — action
- Attribute — state
- Inheritance — is-a
- Composition — has-a
Key takeaways
- Classes bundle state + behavior — they model real-world concepts in code.
- Use @property and private attributes to maintain invariants and make your API pleasant.
- Prefer composition for flexible design; use inheritance for clear "is-a" relationships.
- Dataclasses are your friend when you just need containers for data.
- Write tests inside your venv, and package class-based modules when they're reusable.
"Think of classes as story scripts for your data. If the script is messy, the actors (objects) will improvise badly."
If you want, next I can: provide a small CS50-style project using classes (e.g., a simulated marketplace or a mini-ORM), or show how to refactor a messy script into clean class-based modules ready for packaging.
Comments (0)
Please sign in to leave a comment.
No comments yet. Be the first to comment!