Object-Oriented Programming
Python classes vs C structs + function pointers — encapsulation and inheritance
Introduction
In this lesson, you'll learn about object-oriented programming in Python. Coming from C, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In C, you're familiar with python classes vs c structs + function pointers — encapsulation and inheritance.
Python has its own approach to python classes vs c structs + function pointers — encapsulation and inheritance, which we'll explore step by step.
The Python Way
Let's see how Python handles this concept. Here's a typical example:
import math
from abc import ABC, abstractmethod
# Abstract base class (= C interface via function pointers)
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimeter(self) -> float: ...
def describe(self) -> str:
return f"Area={self.area():.2f}, P={self.perimeter():.2f}"
# Concrete class (= C struct + functions)
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius # instance attribute
def area(self) -> float:
return math.pi * self.radius ** 2
def perimeter(self) -> float:
return 2 * math.pi * self.radius
class Rectangle(Shape):
def __init__(self, w: float, h: float):
self.w, self.h = w, h
def area(self) -> float: return self.w * self.h
def perimeter(self) -> float: return 2 * (self.w + self.h)
# Polymorphism — works on any Shape subclass
def print_shapes(shapes: list[Shape]) -> None:
for s in shapes:
print(s.describe()) # correct method called per type
print_shapes([Circle(5), Rectangle(3, 4)])Comparing to C
Here's how you might have written similar code in C:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
// Simulating OOP in C with structs and function pointers
typedef struct Shape Shape;
struct Shape {
double (*area)(const Shape *self);
double (*perimeter)(const Shape *self);
};
typedef struct {
Shape base; // "inherit" by embedding
double radius;
} Circle;
double circle_area(const Shape *self) {
const Circle *c = (const Circle *)self;
return M_PI * c->radius * c->radius;
}
void circle_init(Circle *c, double radius) {
c->radius = radius;
c->base.area = circle_area;
}
int main(void) {
Circle c;
circle_init(&c, 5.0);
// Polymorphism via function pointer
printf("Area: %.2f\n", c.base.area((Shape*)&c));
return 0;
}You may be used to different syntax or behavior.
Python classes have proper encapsulation — no embedding tricks like C
You may be used to different syntax or behavior.
__init__ is the constructor; no explicit malloc needed
You may be used to different syntax or behavior.
ABC + @abstractmethod is Python's formal interface — stricter than C function pointers
You may be used to different syntax or behavior.
Python inheritance is straightforward: class Dog(Animal)
You may be used to different syntax or behavior.
self is explicit in every method; C uses a pointer passed manually
Step-by-Step Breakdown
1. Class = Struct + Functions
A Python class combines C's struct (data) and its associated functions into one unit. Methods automatically receive self — the equivalent of a struct pointer.
typedef struct { double r; } Circle;
void circle_init(Circle *c, double r) { c->r = r; }class Circle:
def __init__(self, r: float):
self.r = r # like struct field
def area(self):
return math.pi * self.r**2 # self = struct pointer2. Inheritance
Python inheritance is cleaner than C's embedding trick. class Dog(Animal) gives Dog all of Animal's methods automatically.
typedef struct { Animal base; char name[50]; } Dog;
// Must manually forward all Animal's methodsclass Animal:
def speak(self): return "..."
class Dog(Animal):
def speak(self): return "Woof" # override
d = Dog()
d.speak() # "Woof" — no forwarding needed3. Abstract Methods
ABC + @abstractmethod is Python's formal way to require subclasses to implement methods — like C's function pointer fields but compile-time checked.
// C: function pointer in struct — no compile-time check if left NULLfrom abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
# class Square(Shape): pass # Error: must implement area()4. Dunder Methods
Python's dunder (double-underscore) methods let classes define built-in behavior. __repr__, __str__, __eq__, __lt__ are the most useful.
// C: must write compare_circles(), print_circle() manuallyclass Point:
def __init__(self, x, y): self.x, self.y = x, y
def __repr__(self): return f"Point({self.x},{self.y})"
def __eq__(self, o): return self.x==o.x and self.y==o.y
def __lt__(self, o): return (self.x**2+self.y**2) < (o.x**2+o.y**2)
p = Point(1,2)
print(p) # Point(1,2)
p == Point(1,2) # True
points.sort() # uses __lt__Common Mistakes
When coming from C, developers often make these mistakes:
- Python classes have proper encapsulation — no embedding tricks like C
- __init__ is the constructor; no explicit malloc needed
- ABC + @abstractmethod is Python's formal interface — stricter than C function pointers
Key Takeaways
- Class = struct + functions; __init__ = constructor; self = the struct pointer
- Inheritance: class Dog(Animal) — no embedding tricks; all parent methods available
- ABC + @abstractmethod enforces method implementation — like required function pointers
- Dunder methods (__repr__, __eq__, __lt__) customize built-in behavior