Classes in Python (An Introduction)

Next Topic(s):

Created:
4th of November 2025
05:35:07 PM
Modified:
4th of November 2025
05:42:58 PM

Classes in Python (An Introduction)

Objects are everywhere: a mug, a door, a window. Each has properties and behaviour. In programming, a class is a blueprint that describes such objects. Python uses classes to model real-world entities clearly and flexibly, helping us organise code and reason about systems—from day-to-day life to civil engineering models.

What Is a Class?

A class defines the attributes (data) and methods (behaviour) that its objects (instances) will have. In Java you might speak about fields and methods; in Python we say attributes and methods. The idea is the same: bundle related state with the actions that operate on that state.

Daily Example: A Simple Cup

# Defining a simple class
class Cup:
    def __init__(self, colour, capacity_ml):
        self.colour = colour          # attribute (Java: field/member variable)
        self.capacity_ml = capacity_ml
        self.contents_ml = 0

    def fill(self, amount_ml):
        space = self.capacity_ml - self.contents_ml
        self.contents_ml += min(amount_ml, space)
        return self.contents_ml

    def empty(self):
        self.contents_ml = 0

# Using the class (creating instances/objects)
mug = Cup("blue", 300)
mug.fill(120)
print(mug.contents_ml)  # 120

    

Explanation: Cup is the blueprint. mug is an instance with its own state. The __init__ method is Python’s constructor (Java’s constructor equivalent). Attributes such as colour and capacity_ml are bound to each instance via self, which plays a role similar to Java’s this.

💡

Tip: Use clear, domain-specific names for attributes and methods. The class should read like a small, honest description of the real object you are modelling.

From Everyday Objects to Civil Engineering

In buildings, walls contain openings. Doors and windows are both openings with width and height, yet each has extra details: material, fire rating, glazing type, and so on. This is a perfect use-case for inheritance: define a general Opening and extend it for Door and Window. A Wall then contains a set of openings—this is composition.

classDiagram class Opening { +width_mm: int +height_mm: int +area_m2(): float } class Door { +material: str +fire_rating_min: int } class Window { +glass_type: str +is_operable: bool } class Wall { +width_mm: int +height_mm: int +openings: list +net_area_m2(): float +add_opening(o: Opening) } Opening <|-- Door Opening <|-- Window Wall *-- Opening

Example: Modelling Walls, Doors, and Windows

# Base class (general opening in a wall)
class Opening:
    def __init__(self, width_mm, height_mm):
        self.width_mm = width_mm
        self.height_mm = height_mm

    def area_m2(self):
        return (self.width_mm / 1000) * (self.height_mm / 1000)

# Subclasses extend the base opening with extra attributes/behaviour
class Door(Opening):
    def __init__(self, width_mm, height_mm, material, fire_rating_min=0):
        super().__init__(width_mm, height_mm)
        self.material = material
        self.fire_rating_min = fire_rating_min

class Window(Opening):
    def __init__(self, width_mm, height_mm, glass_type, is_operable=True):
        super().__init__(width_mm, height_mm)
        self.glass_type = glass_type
        self.is_operable = is_operable

# Composition: a wall that aggregates openings
class Wall:
    def __init__(self, width_mm, height_mm):
        self.width_mm = width_mm
        self.height_mm = height_mm
        self.openings = []  # list of Opening (Door/Window)

    def add_opening(self, opening):
        self.openings.append(opening)

    def gross_area_m2(self):
        return (self.width_mm / 1000) * (self.height_mm / 1000)

    def openings_area_m2(self):
        return sum(o.area_m2() for o in self.openings)

    def net_area_m2(self):
        return max(0.0, self.gross_area_m2() - self.openings_area_m2())

# Sample usage
wall = Wall(4000, 3000)               # 4.0 m x 3.0 m
door = Door(900, 2100, "timber", 60)  # 0.9 m x 2.1 m fire door
win  = Window(1200, 1200, "double-glazed", True)

wall.add_opening(door)
wall.add_opening(win)

print(f"Gross area (m²): {wall.gross_area_m2():.2f}")
print(f"Openings area (m²): {wall.openings_area_m2():.2f}")
print(f"Net area for finishes (m²): {wall.net_area_m2():.2f}")

    

Explanation: Opening captures common geometry. Door and Window inherit and extend this behaviour. Wall composes openings and computes gross, openings, and net areas—useful for estimating finishes, load checks on non-structural elements, and coordination with schedules.

💡

Tip: Prefer composition (has-a) for assembling systems and inheritance (is-a) for specialisation. Keep the base class small; only place what is truly shared there.

Mapping Terms: Java ↔ Python

Different words, shared ideas. Here is a quick mapping between common Java and Python class terminology.

Class Terminology: Java and Python
Concept Java Term Python Term Usage / Example
Blueprint class class Define once, instantiate many times.
Thing created object / instance object / instance mug = Cup(...)
Data on the object field (member variable) attribute (instance attribute) self.capacity_ml
Behaviour method method def fill(self, ...)
Constructor ClassName(...) __init__(self, ...) Initialise attributes on creation.
Static behaviour static method @staticmethod No access to self.
Class-level behaviour static method / field @classmethod / class attribute Acts on the class, not a single instance.
Inheritance extends class Child(Base) Specialise or reuse behaviour.
Interfaces & contracts interface Abstract Base Class (abc.ABC) Optional in Python; duck typing is common.
Access control public/private/protected Conventions: _internal, __name Name-mangling for __name; rely on discipline.
Properties getters/setters @property Computed attributes with simple syntax.
💡

Trivia: Python emphasises “consenting adults” over strict access modifiers. Instead of hard enforcement, code style and naming conventions guide responsible use.

Adding Calculated Properties with @property

# A window with a computed daylight opening area (example property)
class Window(Opening):
    def __init__(self, width_mm, height_mm, glass_type, frame_thickness_mm=60):
        super().__init__(width_mm, height_mm)
        self.glass_type = glass_type
        self.frame_thickness_mm = frame_thickness_mm

    @property
    def clear_opening_m2(self):
        w = max(0, self.width_mm - 2 * self.frame_thickness_mm)
        h = max(0, self.height_mm - 2 * self.frame_thickness_mm)
        return (w / 1000) * (h / 1000)

win = Window(1200, 1200, "double-glazed", 70)
print(f"Clear opening (m²): {win.clear_opening_m2:.3f}")

    

Explanation: @property lets us expose a calculated value as if it were a simple attribute. In Java we would typically write getClearOpening(); Python keeps the call-site tidy while allowing validation or computation inside the class.

Common Pitfalls

  • Forgetting self in method definitions: Every instance method must accept self as the first parameter.
  • Accidentally sharing state: Do not store per-instance data as class attributes. Use self.attribute in __init__.
  • Overusing inheritance early: Start with composition. Introduce inheritance only when you see genuine shared behaviour.
  • Confusing attributes with properties: Switch to @property when validation or on-the-fly calculation is needed, but keep it simple.

Key Takeaways

  • Use classes to group related data (attributes/fields) and behaviour (methods).
  • Model civil elements naturally: walls have openings; doors and windows are openings.
  • Map your Java mental model to Python terms quickly with the terminology table.
  • Prefer composition; add inheritance when specialisation is obvious.
💡

Tip: For data-heavy classes (schedules, catalogues), consider dataclasses to reduce boilerplate while keeping types explicit.