Yesterday I covered functions in depth: *args, **kwargs, decorators, lambdas. Today I moved into OOP in Python. I already knew OOP from C++, and I know how classes work in JavaScript too, so for today, Day 64, was mostly about learning the Python way of doing things. At the end, I also took a first look at Django Models.
OOP in Python
Classes and Objects
A class is a blueprint. An object is an instance of that blueprint.
class Car:
def __init__(self, brand, model):
self.brand = brand
self.model = model
def describe(self):
return f"{self.brand} {self.model}"
my_car = Car("Toyota", "Corolla")
print(my_car.describe()) # Toyota Corolla
__init__ is the constructor. It runs automatically when you create an object. self is the reference to the current instance, similar to this in JavaScript and C++.
Instance vs Class Attributes
Instance attributes are unique to each object. Class attributes are shared across all instances.
class Employee:
company = "TechCorp" # class attribute
def __init__(self, name):
self.name = name # instance attribute
e1 = Employee("Haris")
e2 = Employee("Ali")
print(e1.company) # TechCorp
print(e2.company) # TechCorp
print(e1.name) # Haris
print(e2.name) # Ali
Instance Methods, Class Methods, and Static Methods
class Circle:
pi = 3.14159
def __init__(self, radius):
self.radius = radius
def area(self): # instance method
return Circle.pi * self.radius ** 2
@classmethod
def from_diameter(cls, diameter): # class method
return cls(diameter / 2)
@staticmethod
def is_valid_radius(r): # static method
return r > 0
c = Circle(5)
print(c.area()) # 78.53975
c2 = Circle.from_diameter(10)
print(c2.radius) # 5.0
print(Circle.is_valid_radius(-1)) # False
-
Instance method — works with instance data, takes
self -
Class method — works with class itself, takes
cls, decorated with@classmethod -
Static method — doesn't need instance or class, decorated with
@staticmethod
Inheritance
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "Some sound"
class Dog(Animal):
def speak(self):
return f"{self.name} says Woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says Meow!"
d = Dog("Bruno")
c = Cat("Whiskers")
print(d.speak()) # Bruno says Woof!
print(c.speak()) # Whiskers says Meow!
super() lets you call the parent class constructor or methods:
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
Encapsulation
Python doesn't have strict private or public keywords like C++, but uses naming conventions:
class BankAccount:
def __init__(self, balance):
self._balance = balance # protected — convention, not enforced
self.__pin = 1234 # private — name mangled by Python
def get_balance(self):
return self._balance
-
_name— protected by convention, accessible but signals "don't touch" -
__name— Python mangles it to_ClassName__name, harder to access from outside
Polymorphism
Same interface, different behavior depending on the object.
animals = [Dog("Bruno"), Cat("Whiskers")]
for animal in animals:
print(animal.speak())
# Bruno says Woof!
# Whiskers says Meow!
Dunder (Magic) Methods
These let you define how your objects behave with Python's built-in operations.
class Book:
def __init__(self, title, pages):
self.title = title
self.pages = pages
def __str__(self):
return f"{self.title} ({self.pages} pages)"
def __len__(self):
return self.pages
def __eq__(self, other):
return self.pages == other.pages
b1 = Book("Clean Code", 431)
b2 = Book("The Pragmatic Programmer", 431)
print(str(b1)) # Clean Code (431 pages)
print(len(b1)) # 431
print(b1 == b2) # True
Common ones worth knowing:
-
__str__— called byprint()andstr() -
__len__— called bylen() -
__eq__— called by== -
__repr__— official string representation, used in debugging
Abstract Classes
When you want to define a blueprint that forces subclasses to implement certain methods:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Rectangle(Shape):
def __init__(self, w, h):
self.w = w
self.h = h
def area(self):
return self.w * self.h
r = Rectangle(4, 5)
print(r.area()) # 20
You can't instantiate Shape directly; it is purely a contract for subclasses.
Introduction to Models in Django
Now that OOP is clear, Django Models will make a lot more sense because a Django Model is literally just a Python class.
What is a Model?
A model in Django represents a database table. Each attribute of the class maps to a column in that table. Django handles all the SQL for you.
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
This single class tells Django to create a post table with columns: title, content, and created_at.
Common Field Types
| Field | Use |
|---|---|
CharField |
Short text with a max length |
TextField |
Long text, no max length |
IntegerField |
Integer numbers |
BooleanField |
True or False |
DateTimeField |
Date and time |
ForeignKey |
Relationship to another model |
Migrations
After defining your model, you don't write SQL. You run two commands:
python manage.py makemigrations
python manage.py migrate
makemigrations generates the migration file based on your model changes. migrate applies those changes to the actual database.
The __str__ Method
Notice the __str__ method in the model above. This is why OOP matters in Django — it controls how your object is displayed in the Django admin panel and shell.
def __str__(self):
return self.title
Without this, Django just shows something like Post object (1) which tells you nothing.
Wrapping Up
OOP clicked better today now that I'm seeing how directly it connects to Django. Models are just classes, fields are just attributes, and methods like __str__ actually serve a real purpose in the framework.
Thanks for reading. Feel free to share your thoughts!
Top comments (0)