UML Class Diagrams in Python
Learn to read and create UML class diagrams by writing Python code that matches target diagrams. Starting from single classes and building to full system architectures, you will discover how UML makes invisible design decisions visible.
Your First Class Diagram
Welcome to UML Class Diagrams
Learning objective: After this step you will be able to identify the three compartments of a UML class box and write a Python class that matches a given diagram.
💡 Light mode recommended. The UML diagrams in this tutorial are easier to read on a light background. If you are in dark mode, consider switching with the Dark mode toggle in the tutorial navbar.
Heads up — learning UML feels weird at first. You are about to map two things that look very different: boxes with symbols on one side, Python code on the other. The first few connections take effort to see. If a notation feels arbitrary, that’s normal — keep going. By Step 4 you’ll be reading diagrams as fluently as you read code.
What Is a UML Class Diagram?
A UML class diagram is a visual blueprint of your software’s structure. It shows what classes exist, what data they hold, what behavior they provide, and how they relate to each other. Think of it as a floor plan — you can understand the building without inspecting every brick.
The Three Compartments
Every class in UML is drawn as a box with three sections:
| Compartment | Contains | Python Equivalent |
|---|---|---|
| Top | Class name | class ClassName: |
| Middle | Attributes (data) | Instance variables in __init__ |
| Bottom | Methods (behavior) | Method definitions |
Your Target Diagram
Write Python code until the live diagram below matches this target:
Reading the Diagram
- Top: The class name is
Student→class Student: - Middle: Two attributes
nameandstudent_id→ instance variables set in__init__ - Bottom: One method
get_info()→ a method definition
That is all there is to it — the diagram is a visual summary of the class.
Note: You may see symbols like
+,-, and types like: strin other UML diagrams. We will cover those in the next steps. For now, focus on the three compartments.
Your Task
Open student.py and create a Student class that:
- Defines a constructor
__init__(self, name, student_id) - Stores both parameters as instance attributes (
self.name = name) - Has a
get_info()method returning"name (student_id)"— for example"Alice (S001)"
Watch the UML Diagram panel — it updates live as you type!
# Your task: create a Student class that matches the target diagram.
#
# The class needs:
# - An __init__ that accepts name and student_id
# - Both stored as instance attributes
# - A get_info() method returning "name (student_id)"
The Class Box
Min. score: 60%1. What does the middle compartment of a UML class box show?
The three compartments are: top = class name, middle = attributes, bottom = methods. Relationships are shown as arrows between class boxes, not inside them.
2. A Python class has self.x = 10 inside a def calculate(self) method. How many items appear in the UML class box?
The UML box has three compartments: the class name at the top, x in the attributes section (middle), and calculate() in the methods section (bottom). self is not shown in UML — it is implicit.
3. Predict before you run. Given this Python code, how many items will appear in the bottom (methods) compartment of the UML box?
class Timer:
def __init__(self, seconds):
self.seconds = seconds
self.running = False
def start(self):
self.running = True
def stop(self):
self.running = False
The bottom compartment lists methods. Timer defines three: __init__, start, and stop. The attributes seconds and running go in the middle compartment, not the bottom. Predicting before you run is a powerful way to test your mental model — you either confirm it or you find the gap.
Visibility: Who Can See What?
Visibility Markers
Learning objective: After this step you will be able to map UML visibility symbols to Python naming conventions and explain why encapsulation is a design decision.
The Four UML Visibility Levels
UML uses symbols to show who can access each attribute or method (source: UML@Classroom, Seidl et al., Table 4.1):
| UML Symbol | Meaning | Access Scope |
|---|---|---|
+ |
Public | Any object in the system |
- |
Private | Only the implementing class itself |
# |
Protected | The class and its subclasses |
~ |
Package | Classes in the same package |
Python Is Different — and That’s Part of the Lesson
Unlike Java or C++, Python has no private or protected keywords. Access control in Python is entirely convention-based. This tutorial uses the following Python-to-UML mapping that the live diagram renderer recognises:
| UML | Python (as read by this renderer) |
|---|---|
+ Public |
self.name (no prefix) |
# Protected |
self._name (single leading underscore) |
- Private |
self.__name (double leading underscore) |
What _ and __ Really Mean in Python
Single underscore _ — the “internal use” signal (PEP 8)
self._internal_cache = [] # "Implementation detail — don't rely on this"
A leading _ is a social contract. Python does nothing to enforce it; tools like from module import * skip these names, and the broader community treats them as non-public. Most Pythonistas use _ to mean “non-public” whether the intent is protected or private.
Double underscore __ — name mangling, NOT privacy
self.__balance = 100
Python rewrites __balance to _BankAccount__balance. Per the official Python tutorial:
“Name mangling is intended to give classes an easy way to define ‘private’ instance variables… without having to worry about instance variables defined by derived classes.”
The primary purpose of __ is avoiding name clashes in deep inheritance hierarchies (PEP 8), not privacy. It happens to make accidental external access harder, which is why many tools (and this renderer) treat it as the closest Python analog of UML -. But don’t reach for __ just to “make something private” — idiomatic Python rarely uses it.
account = BankAccount(100)
account.__balance # AttributeError (mangled)
account._BankAccount__balance # Works — a determined caller can always get in
Key takeaway: UML visibility expresses design intent; Python conventions express that intent through naming, not enforcement. In this tutorial we use
__for private so the UML renderer displays-, but in real Python code many teams standardise on_for anything non-public.
Why This Matters
Python does not enforce visibility — but UML forces you to decide what should be accessible. When you model a class in UML, you make a deliberate architectural choice about which parts are the public interface and which are internal implementation details that could change without warning.
Your Target Diagram
Your Task
The starter code has a BankAccount where everything is public. Refactor it:
- Make
balanceprivate → rename to__balance(matches-in UML) - Make
validate_amountprotected → rename to_validate_amount(matches#) - Keep
deposit,withdraw, andget_balancepublic (they stay as-is) - Update all internal references to use the new names
Watch the UML diagram update — the visibility markers should change from + to - and #.
class BankAccount:
"""A bank account — but everything is public!
Your job: apply proper visibility using Python naming conventions."""
def __init__(self, initial_balance: float) -> None:
self.balance: float = initial_balance # Should be private (-)
def deposit(self, amount: float) -> None:
if self.validate_amount(amount): # Update reference
self.balance += amount # Update reference
def withdraw(self, amount: float) -> bool:
if self.validate_amount(amount) and self.balance >= amount:
self.balance -= amount # Update reference
return True
return False
def get_balance(self) -> float:
return self.balance # Update reference
def validate_amount(self, amount: float) -> bool: # Should be protected (#)
return amount > 0
if __name__ == "__main__":
account = BankAccount(100.0)
account.deposit(50.0)
print(f"Balance: ${account.get_balance():.2f}")
account.withdraw(30.0)
print(f"Balance: ${account.get_balance():.2f}")
Visibility Notation
Min. score: 60%
1. In UML, what does the - symbol before an attribute mean?
- means private — only accessible within the class itself. In Python, this maps to the double-underscore prefix (__), which triggers name mangling.
2. A Python method named _calculate_tax would appear in UML with which visibility marker?
A single leading underscore (_) is the Python convention for protected members, which maps to # in UML. Double underscores (__) map to private (-).
Types Matter: Explicit Contracts
Explicit Types in UML
Learning objective: After this step you will be able to add Python type hints that make UML diagrams show complete type information and explain why explicit types improve software design.
What Are Type Hints?
You may not have seen Python type hints before. They are optional annotations that tell both humans and tools what type a variable or return value should be:
# Without type hints (what you are used to):
def __init__(self, name, price):
self.name = name
# With type hints:
def __init__(self, name: str, price: float) -> None:
self.name: str = name
| Syntax | Meaning | Example |
|---|---|---|
param: Type |
Parameter has this type | name: str |
self.x: Type = value |
Attribute has this type | self.name: str = name |
-> Type |
Method returns this type | def get_price(self) -> float: |
-> None |
Method returns nothing | def __init__(self, ...) -> None: |
Type hints do not change how Python runs your code — Python ignores them at runtime. But they serve two critical purposes:
- UML diagrams — the live diagram renderer reads type hints to show types. Without them, the diagram only shows names.
- Communication — type hints document the contracts of your class for other developers.
(Type hints can also be enforced at build time with tools like mypy. That’s a topic for another tutorial — see the reference at the end of this one for a pointer.)
The Problem with Duck Typing
Python is dynamically typed — you can write def get_price(self) without ever specifying that it returns a float. This flexibility is convenient, but it hides the contracts between components. Another developer reading your code has to trace through the logic to figure out what types flow where.
UML does not allow this ambiguity. Every attribute must show its type, and every method must show its parameter types and return type.
UML Type Notation
| UML | Python |
|---|---|
- name: str |
self.__name: str = name |
+ get_price(): float |
def get_price(self) -> float: |
+ apply_discount(percent: float): float |
def apply_discount(self, percent: float) -> float: |
Your Target Diagram
Your Task
The starter code works perfectly — but has zero type hints. The UML diagram shows the class without any type information. Add type hints to:
- All
__init__parameters - All instance attributes (e.g.,
self.__name: str = name) - All method return types (e.g.,
-> float) - All method parameters (e.g.,
percent: float)
Watch the UML diagram fill in with types as you add annotations.
class Product:
"""A product in an online store.
Everything works — but there are no type hints!
Add type annotations so the UML diagram shows types."""
def __init__(self, name, price, in_stock):
self.__name = name
self.__price = price
self.__in_stock = in_stock
def get_name(self):
return self.__name
def get_price(self):
return self.__price
def is_available(self):
return self.__in_stock
def apply_discount(self, percent):
discount = self.__price * (percent / 100)
return self.__price - discount
if __name__ == "__main__":
p = Product("Laptop", 999.99, True)
print(f"{p.get_name()}: ${p.get_price():.2f}")
print(f"After 10% off: ${p.apply_discount(10):.2f}")
print(f"In stock: {p.is_available()}")
Types in UML
Min. score: 60%1. Why does UML require explicit types on all attributes and methods?
UML forces explicit types to document the contracts — what data flows between components and in what form. This is a design decision that improves communication, regardless of whether the language enforces it.
2. How does the UML notation + apply_discount(percent: float): float map to Python?
Python methods always include self as the first parameter, but UML omits it (it is implied). The return type goes after -> in Python, and after : in UML. Both percent: float parameter annotations match directly.
Inheritance: Is-A Relationships
The Generalization Arrow
Learning objective: After this step you will be able to refactor duplicated classes into an inheritance hierarchy and draw the correct generalization arrow direction.
Heads up — the arrow direction trips up almost everyone the first time. Even developers who use inheritance every day sometimes have to pause and think. Expect to re-read the “Is-a test” below once or twice. That is the skill forming, not a sign you’re confused.
Inheritance in UML
When a class extends another class (an “is-a” relationship), UML draws a solid line with a hollow triangle pointing at the parent (superclass):
Child Parent
⚠ Common mistake: Students often draw the triangle pointing away from the parent, from superclass down to subclass. The correct direction is the opposite: the child points up to the parent.
“Is-a” test: Before drawing, check the sentence “A [Child] is a [Parent]” makes sense. “A Dog is an Animal” → yes. “An Animal is a Dog” → no. The inheriting class is the subject; the triangle points at the parent.
Your Target Diagram
Notice: Circle and Rectangle only list their own attributes. They inherit color and describe() from Shape — they do not repeat them.
Your Task
The starter code has three independent classes with duplicated color and describe(). Refactor them:
- Make
Shapethe base class withcolor,area(), anddescribe() - Make
CircleandRectangleinherit fromShapeusingclass Circle(Shape): - Remove the duplicated
colorattribute anddescribe()method from the subclasses - Each subclass should call
super().__init__(color)and overridearea()
Watch the inheritance arrows appear in the live diagram.
import math
class Shape:
def __init__(self, color: str) -> None:
self.color: str = color
def area(self) -> float:
return 0.0
def describe(self) -> str:
return f"{self.color} shape with area {self.area():.2f}"
class Circle:
"""Independent class — duplicates color and describe from Shape!"""
def __init__(self, color: str, radius: float) -> None:
self.color: str = color # Duplicated!
self.radius: float = radius
def area(self) -> float:
return math.pi * self.radius ** 2
def describe(self) -> str: # Duplicated!
return f"{self.color} shape with area {self.area():.2f}"
class Rectangle:
"""Independent class — duplicates color and describe from Shape!"""
def __init__(self, color: str, width: float, height: float) -> None:
self.color: str = color # Duplicated!
self.width: float = width
self.height: float = height
def area(self) -> float:
return self.width * self.height
def describe(self) -> str: # Duplicated!
return f"{self.color} shape with area {self.area():.2f}"
if __name__ == "__main__":
c = Circle("red", 5.0)
r = Rectangle("blue", 3.0, 4.0)
print(c.describe())
print(r.describe())
Inheritance Arrows
Min. score: 60%1. In a UML class diagram, which direction does the inheritance arrow point?
The generalization arrow always points from the child to the parent — the hollow triangle is at the parent end. Think of it as the child “reaching up” to the thing it extends.
2. If Circle inherits describe() from Shape, where does describe() appear in the UML diagram?
Inherited members appear only in the parent class box. The child class only lists members it adds or overrides. The inheritance arrow tells you that everything in the parent is available in the child.
3. Review of Step 2. Given the Shape class + color: str and an inherited subclass Circle that needs to read color in its area() method, which access level is most appropriate for color if we want subclasses to read it but external code not to?
# protected is the classic “I need subclasses to see this, but not arbitrary outside code” visibility. If color were private (-), Circle could not access it directly. This question reconnects Step 2’s visibility markers with Step 4’s inheritance — UML concepts are not independent; they interact.
Association: Classes That Know Each Other
Association Arrows
Learning objective: After this step you will be able to identify when a UML association exists between two classes and refactor string-based references into proper object associations.
What Is an Association?
An association means one class stores a reference to another class as an instance variable. In UML, this is drawn as a solid arrow from the class that holds the reference to the class it references.
The key rule: If a class stores another object as a persistent instance variable (self.instructor: Instructor), that is an association. If it only uses another class temporarily inside a method, that is a weaker relationship (a dependency, which we will skip for now).
Your Target Diagram
Notice the association arrow from Course to Instructor — it appears because Course has an instructor: Instructor attribute.
Your Task
The starter code stores the instructor as a plain string (instructor_name: str). This hides the relationship — the UML shows no connection between the classes.
- Create an
Instructorclass withname: str,department: str, and aget_title()method returning"name (department)" - Refactor
Courseto accept and store anInstructorobject instead of a string - Update
get_instructor_name()to returnself.instructor.name
Watch the association arrow appear in the UML diagram!
class Course:
"""A course — but the instructor is just a string!
There is no Instructor class, so the UML shows no relationship."""
def __init__(self, name: str, instructor_name: str) -> None:
self.name: str = name
self.instructor_name: str = instructor_name # Just a string!
def get_instructor_name(self) -> str:
return self.instructor_name
# TODO: Create an Instructor class with name, department, and get_title()
# TODO: Refactor Course to store an Instructor object instead of a string
if __name__ == "__main__":
# After your refactoring, this code should work:
# instructor = Instructor("Dr. Smith", "Computer Science")
# course = Course("CS 101", instructor)
# print(f"{course.name} taught by {course.get_instructor_name()}")
course = Course("CS 101", "Dr. Smith")
print(f"{course.name} taught by {course.get_instructor_name()}")
Associations
Min. score: 60%1. When does an association arrow appear between two classes in a UML diagram?
An association arrow appears when a class stores another object as a persistent instance variable (e.g., self.instructor: Instructor). Simply importing or calling a method creates a weaker dependency, not an association.
2. Why is storing instructor_name: str worse than instructor: Instructor from a design perspective?
When you use a string, the relationship between Course and Instructor is invisible — both in the code and in the UML diagram. Using an Instructor object makes the dependency explicit, allowing UML to show the arrow and helping other developers understand the system structure at a glance.
3. Review of Step 3. In the solution above, Course stores self.instructor: Instructor = instructor. Why is the : Instructor type annotation load-bearing — what would change if you wrote self.instructor = instructor instead?
Python itself ignores type annotations at runtime — but the UML renderer reads them. Without : Instructor, the renderer can’t tell what class the attribute refers to, and the association arrow disappears. This reconnects Step 3’s “types as contracts” lesson with Step 5’s “relationships as visibility”: both rely on the same annotations.
Composition vs Aggregation
Ownership and Lifecycle
Learning objective: After this step you will be able to distinguish composition from aggregation and implement both patterns correctly in Python.
Heads up — this is the distinction working developers most often get wrong. If the rule feels fuzzy after this step, that is honest confusion, not a learning failure — the UML spec itself calls aggregation’s semantics “intentionally informal.”
Warm-Up (Retrieval from Step 5)
Before you read on — close your eyes for five seconds and answer: in Step 5, what exactly made the UML association arrow appear between
CourseandInstructor? Was it importing the class, storing an instance as an attribute, calling a method, or something else? Pick the answer you would bet on, then check the next paragraph.
An association appears when a class stores another object as a persistent instance variable — not when it merely imports or uses it. Keep that rule in your head: this step’s composition and aggregation are both special cases of it.
Two Kinds of “Has-A”
Both composition and aggregation model a “whole-part” relationship. The difference is ownership and lifecycle:
| Aspect | Composition (filled diamond) | Aggregation (hollow diamond) |
|---|---|---|
| Symbol | filled diamond | hollow diamond |
| Ownership | Whole owns the part exclusively (no sharing) | Whole references the part (can be shared) |
| Lifecycle | Part is destroyed with the whole | Part survives independently |
| Python pattern | Part created inside __init__ |
Part passed in from outside |
Honest caveat. Composition has sharp semantics in the UML spec: a part belongs to exactly one composite at a time, and is deleted with it. Aggregation, however, is deliberately fuzzy — the UML 2 specification calls its semantics “intentionally informal”. For this tutorial we’ll use the common textbook interpretation (conceptual whole-part relationship). Aggregation is a domain decision, not a code decision. Whether a relationship is aggregation or plain association cannot be read reliably from code alone — it depends on the meaning of the domain. Is a professor a part of a department or does a department merely know some professors? That answer comes from domain knowledge, not from Python syntax. This tutorial’s live diagram uses heuristics, which works well as a learning scaffold — but in the real world, rely on domain knowledge rather than on tools to infer it.
The File System Metaphor
- Composition = a directory and its files. If you run
rm -rf directory/, the files inside are destroyed. Their lifecycle is bound to the directory. - Aggregation = a directory containing symbolic links. If you delete the directory, the symlinks vanish but the original files they pointed to survive.
Your Target Diagram
Notice the two different diamonds:
- Filled diamond between University and Department → composition. The university creates its departments. If the university ceases to exist, so do its departments.
- Hollow diamond between Department and Professor → aggregation. Professors are independent people who are assigned to departments. If a department is dissolved, the professors still exist.
Note: You may notice that the live diagram does not show how many departments or professors participate. Those numbers (called multiplicity) are covered in the next step.
Your Task
Complete the starter code:
University.add_department(dept_name)should create a newDepartmentinternally (composition — the part is born inside the whole)Department.add_professor(prof)should receive an existingProfessorfrom outside (aggregation — the part exists independently)
class Professor:
def __init__(self, name: str, field: str) -> None:
self.name: str = name
self.field: str = field
class Department:
def __init__(self, name: str) -> None:
self.name: str = name
self.professors: list[Professor] = []
def add_professor(self, prof: Professor) -> None:
# TODO: Store the professor (aggregation — received from outside)
pass
class University:
def __init__(self, name: str) -> None:
self.name: str = name
self.departments: list[Department] = []
def add_department(self, dept_name: str) -> None:
# TODO: Create a new Department and add it (composition — created inside)
pass
def get_department(self, name: str) -> Department:
for dept in self.departments:
if dept.name == name:
return dept
raise ValueError(f"Department '{name}' not found")
if __name__ == "__main__":
# Professors exist independently — they are created outside
prof_alice = Professor("Dr. Alice", "AI")
prof_bob = Professor("Dr. Bob", "Systems")
# University creates its own departments (composition)
uni = University("State University")
uni.add_department("Computer Science")
uni.add_department("Mathematics")
assert len(uni.departments) == 2, "add_department needs to actually store the new department"
# Professors are assigned to departments (aggregation)
cs = uni.get_department("Computer Science")
cs.add_professor(prof_alice)
cs.add_professor(prof_bob)
assert len(cs.professors) == 2, "add_professor needs to store the received professor"
print(f"{uni.name} has {len(uni.departments)} departments")
print(f"CS has {len(cs.professors)} professors")
Composition vs Aggregation
Min. score: 60%
1. A Car creates its own Engine in __init__. If the car is scrapped, the engine goes with it. What UML relationship is this?
This is composition (filled diamond). The engine is created inside the car and its lifecycle is bound to the car. If the car is destroyed, the engine is too. The key indicator: the part is created internally, not passed in.
2. A Team holds references to Player objects that were created outside the team. Players can be traded to other teams. What UML relationship is this?
This is aggregation (hollow diamond). Players exist independently of any team — they were created outside, passed in, and can move to another team. The team holds a reference but does not control the player’s lifecycle.
3. What Python code pattern signals composition?
Composition means the whole creates the part internally: self.part = Part(...). The part’s lifecycle is tied to the whole. Aggregation means the part is passed in from outside: def __init__(self, part: Part).
Multiplicity: How Many?
Multiplicity Notation
Learning objective: After this step you will be able to read multiplicity notation on UML associations and implement one-to-many relationships using Python lists.
What Is Multiplicity?
Multiplicity tells you how many instances participate in a relationship. It is written as a number or range next to each end of an association line.
| Notation | Meaning | Equivalent |
|---|---|---|
1 |
Exactly one | |
0..1 |
Zero or one (optional) | |
* (or 0..*) |
Zero or more | a collection that may be empty |
1..* |
One or more | a collection that must have at least one element |
Style tip: Prefer
*over verbose0..*. The UML spec defines them as identical, and*is the more concise and widely recognized shorthand. Use the explicit0..*only when you want to emphasize the lower bound in context (e.g., contrasting it with1..*nearby).
Reading Multiplicity as a Sentence
Read from each end toward the other. Multiplicity sits next to the class end it quantifies:
Playlist “0..*“ Song
- Left-to-right: “One
Playlistcontains zero or moreSongs.” - Right-to-left: “Each
Songbelongs to somePlaylist” — but we can’t say how many from a diagram with only one multiplicity shown.
⚠ Unidirectional diagrams only tell half the story. When the Playlist end is blank, the Song-to-Playlist multiplicity is unspecified, not “1.” In a real music app a song typically lives on many playlists — modeling that requires a multiplicity at the Playlist end too (e.g.,
Playlist "0..*" <-- "*" Song). This tutorial keeps one end hidden to teach one idea at a time; real designs usually show both.
Placement rule: The number sits next to the class it quantifies. The 0..* goes next to Song because one playlist has many songs, not because there are “many songs in general.”
⚠ Common mistake (Chren et al., 2019): Beginners flip the multiplicities — putting
*next to the playlist end to mean “there are many playlists.” That is wrong. Multiplicity always answers: “For one instance of the opposite class, how many of this class participate?”
Your Target Diagram
Your Task
The starter code has a Playlist that holds a single Song. Refactor it to hold many songs:
- Change
self.songtoself.songs: list[Song] = [](a list of songs) - Add an
add_song(song: Song)method that appends to the list - Add
get_total_duration()returning the sum of all song durations - Add
get_song_count()returning the number of songs
The * multiplicity means the playlist can have zero or more songs.
class Song:
def __init__(self, title: str, artist: str, duration_sec: int) -> None:
self.title: str = title
self.artist: str = artist
self.duration_sec: int = duration_sec
class Playlist:
"""Currently holds a single song. Refactor to hold many songs!"""
def __init__(self, name: str, song: Song) -> None:
self.name: str = name
self.song: Song = song # Only ONE song — change to a list!
if __name__ == "__main__":
s1 = Song("Bohemian Rhapsody", "Queen", 354)
p = Playlist("Road Trip", s1)
print(f"Playlist: {p.name}")
Multiplicity
Min. score: 60%
1. In UML, Department "1" --> "1..*" Employee — where is the * placed and why?
The multiplicity is placed next to the class it quantifies. There are many employees per department, so 1..* goes next to Employee. There is one department per group, so 1 goes next to Department.
2. What does the multiplicity 0..1 mean?
0..1 means the relationship is optional — there can be zero or one instance. For example, a Person might have 0..1 Passport — not everyone has a passport, but no one has two.
3. Review of Step 6. A University has 1..* Departments and a Department has 1..* Professors. Given the lifecycle rules you learned in Step 6, which pair of diamonds is correct?
Multiplicity tells you how many participate; the diamond tells you ownership and lifecycle. They are independent decisions. Here you combine Step 6’s lifecycle reasoning with Step 7’s multiplicity notation — both pieces of information go on the same arrow in the diagram.
Abstract Classes: Designing for Extension
Abstract Classes in UML
Learning objective: After this step you will be able to implement abstract classes using Python’s
abcmodule and recognize italic notation in UML diagrams.
Flashback to Step 4
Remember Step 4’s Shape?
class Shape:
def area(self) -> float:
return 0.0 # ← wait, what is the area of a generic "shape"?
That 0.0 was always a lie. A Shape isn’t a thing you can actually measure — only specific shapes (circles, rectangles) have areas. We hid the lie behind a default value and let Circle and Rectangle override it. That worked, but it left a bug-shaped hole: if you ever wrote Shape("red").area(), Python cheerfully returned 0.0 instead of telling you that you made a design mistake.
Abstract classes are how you fix that hole. By the end of this step, you will know how to say “this class is a blueprint; you must not instantiate it directly, and every subclass must implement these methods.”
What Is an Abstract Class?
An abstract class is a class that cannot be instantiated directly — it serves as a blueprint that subclasses must complete. In UML, abstract classes and abstract methods are shown in italics.
Python’s abc Module
Python does not have an abstract keyword like Java or C++. Instead, you use the abc (Abstract Base Classes) module:
from abc import ABC, abstractmethod
class Shape(ABC): # Inherit from ABC
@abstractmethod # Mark as abstract
def area(self) -> float:
pass # No implementation
Trying to instantiate Shape() directly will raise a TypeError.
Your Target Diagram
Notice: PaymentMethod and its methods appear in italics — this signals they are abstract.
Your Task
The starter code has a concrete PaymentMethod base class. Make it abstract:
- Import
ABCandabstractmethodfrom theabcmodule - Make
PaymentMethodinherit fromABC - Mark
process()andget_name()with@abstractmethod - Complete the
CreditCardandBankTransfersubclasses
# TODO: Import ABC and abstractmethod from the abc module
class PaymentMethod:
"""This should be abstract — you should NOT be able to create
a plain PaymentMethod(). Make it inherit from ABC."""
def process(self, amount: float) -> bool:
# This should be abstract — mark with @abstractmethod
return False
def get_name(self) -> str:
# This should be abstract — mark with @abstractmethod
return "Unknown"
class CreditCard(PaymentMethod):
def __init__(self, card_number: str) -> None:
self.card_number: str = card_number
# TODO: Implement process() — print and return True
# TODO: Implement get_name() — return "Credit Card"
class BankTransfer(PaymentMethod):
def __init__(self, account_number: str) -> None:
self.account_number: str = account_number
# TODO: Implement process() — print and return True
# TODO: Implement get_name() — return "Bank Transfer"
if __name__ == "__main__":
cc = CreditCard("4111-1111-1111-1111")
bt = BankTransfer("DE89370400440532013000")
print(f"Paying with {cc.get_name()}: {cc.process(49.99)}")
print(f"Paying with {bt.get_name()}: {bt.process(150.00)}")
Abstract Classes
Min. score: 60%1. What does italic text on a class name in UML indicate?
Italic text in UML indicates abstract — the class (or method) cannot be used directly and must be implemented by a subclass. In Python, this is achieved using ABC and @abstractmethod.
2. What happens if a Python class inherits from an abstract class but does NOT implement all abstract methods?
Python raises a TypeError at instantiation time if any @abstractmethod is not implemented. This enforces the contract defined by the abstract class — you cannot create an incomplete implementation.
3. Review of Step 4. In the target diagram for this step, which direction does the triangle point between CreditCard and PaymentMethod?
The hollow triangle of a generalisation arrow always points at the parent/superclass — here, PaymentMethod. The child class (CreditCard) is at the non-triangle end. This is one of the most commonly reversed notations in student diagrams (Chren et al., 2019). “A CreditCard is a PaymentMethod” — the sentence order mirrors the arrow direction.
The Fixer-Upper: Diagnose a Bad Design
The God Class Anti-Pattern
Learning objective: After this step you will be able to identify the God Class anti-pattern in a UML diagram and refactor a monolithic class into cohesive, collaborating classes.
Spotting the Problem
Look at the UML diagram for the starter code. You will see ONE massive class with dozens of attributes and methods, and no other classes at all. This is called a God Class (also known as “The Blot”) — a single class that tries to do everything.
In a UML diagram, the God Class is easy to spot: one huge box surrounded by nothing. No relationships, no collaboration, no distribution of responsibility.
Why It Matters
A God Class is invisible in 500 lines of Python — you might not realize how bloated it is until you try to modify it. But in a UML diagram, the problem screams at you. This is one of the most valuable uses of UML: making bad architecture visible before it becomes a maintenance nightmare.
Your Target Diagram
Refactor the monolithic OnlineStore into this well-structured system:
New Notation: Dependency
The diagram introduces one arrow you have not learned before: the dashed arrow ().
| Symbol | Name | Meaning | Python Pattern |
|---|---|---|---|
| Dependency | “temporarily uses” — the weakest link | A class appears only as a method parameter or local variable — never stored in self |
In the target diagram, OnlineStore ..> Customer means OnlineStore uses Customer only inside place_order() — as a method parameter that is immediately handed off to Order. There is no self.customer attribute on OnlineStore; the Customer object passes through and leaves.
Rule of thumb:
self.x: Other = other→ association / composition / aggregation (persistent reference)def method(self, other: Other)orlocal = Other(...)inside a method, never stored → dependency (temporary use)
This is the weakest possible relationship — the dashed line signals “I know this class exists, but I do not hold onto it.”
Your Task
The starter code is a single OnlineStore class that manages products, customers, orders, and notifications all by itself. Refactor it:
- Extract
Product— name, price, stock,is_available(),reduce_stock() - Extract
Customer— name, email - Extract
Order— stores customer and items, calculates total - Slim down
OnlineStore— coordinates the other classes
Watch the UML diagram transform from a single blob into an interconnected network.
class OnlineStore:
"""THE GOD CLASS — does everything, knows everything, fears nothing.
Look at the UML diagram: one giant box, no collaborators.
Your mission: extract Product, Customer, and Order classes."""
def __init__(self) -> None:
# Product data (should be its own class)
self._product_names: list[str] = []
self._product_prices: list[float] = []
self._product_stocks: list[int] = []
# Order data (should be its own class)
self._order_customer_names: list[str] = []
self._order_customer_emails: list[str] = []
self._order_items: list[Product] = []
self._order_totals: list[float] = []
# ── Product management ──────────────────────────────────
def add_product(self, name: str, price: float, stock: int) -> None:
self._product_names.append(name)
self._product_prices.append(price)
self._product_stocks.append(stock)
def is_product_available(self, name: str) -> bool:
idx = self._product_names.index(name)
return self._product_stocks[idx] > 0
def get_product_price(self, name: str) -> float:
idx = self._product_names.index(name)
return self._product_prices[idx]
def reduce_product_stock(self, name: str) -> None:
idx = self._product_names.index(name)
self._product_stocks[idx] -= 1
# ── Order management ────────────────────────────────────
def place_order(self, customer_name: str, customer_email: str,
product_names: list) -> int:
total = 0.0
for pname in product_names:
total += self.get_product_price(pname)
self.reduce_product_stock(pname)
self._order_customer_names.append(customer_name)
self._order_customer_emails.append(customer_email)
self._order_items.append(product_names)
self._order_totals.append(total)
order_id = len(self._order_totals) - 1
print(f"[EMAIL] To: {customer_email} | Order #{order_id} confirmed: ${total:.2f}")
return order_id
def get_order_total(self, order_id: int) -> float:
return self._order_totals[order_id]
if __name__ == "__main__":
store = OnlineStore()
store.add_product("Laptop", 999.99, 5)
store.add_product("Mouse", 29.99, 50)
store.add_product("Keyboard", 79.99, 30)
order_id = store.place_order("Alice", "alice@example.com",
["Laptop", "Mouse"])
print(f"Order total: ${store.get_order_total(order_id):.2f}")
God Class & Comprehensive Review
Min. score: 60%1. How can you spot a God Class in a UML diagram?
A God Class appears as a single massive box with dozens of attributes and methods, with few or no collaborating classes around it. The lack of relationships in the diagram signals that one class is doing everything — the opposite of good object-oriented design.
2. How does UML help you detect design problems that are hard to see in code?
UML makes architecture visible. A God Class is invisible in 500 lines of Python — you might not notice the bloat. But in a UML diagram, one enormous box surrounded by nothing is immediately obvious. UML is a thinking tool, not just documentation.
3. Match the UML notation to its meaning: a solid line with a filled diamond on one end.
A filled diamond means composition — the whole exclusively owns the part, and the part is destroyed when the whole is destroyed. A hollow diamond would mean aggregation (independent lifecycle).
4. A Course class stores self.instructor: Instructor = instructor where the instructor is passed in from outside. Why is this an association rather than composition?
The Instructor exists independently — it was created outside of Course and passed in. Deleting a course does not delete the instructor. This is a reference, not ownership, so it is an association (plain arrow) rather than composition (filled diamond).
5. What does italic text on a class name in a UML diagram indicate?
Italic text in UML indicates abstract — the class cannot be instantiated and must be subclassed. In Python, this is achieved with class Name(ABC): and @abstractmethod.
6. In UML, Department "1" --> "*" Employee — what does * next to Employee mean?
The multiplicity * is placed next to Employee because it quantifies how many employees a department can have: zero or more. Read it as a sentence: “One Department has zero or more Employees.”
7. What is the most important purpose of a UML class diagram?
The primary purpose of UML is communication. A class diagram lets developers understand and discuss the architecture of a system — what classes exist, how they relate, and what contracts they define — without reading every line of code. It is a thinking and communication tool, not a replacement for code.
UML Class Diagram Reference
Congratulations!
You have learned to read and create UML class diagrams. This final page summarizes every notation element covered in this tutorial — use it as a quick reference.
The Class Box
Every class is drawn as a box with three compartments:
| Compartment | Contains | Python |
|---|---|---|
| Top | Class name | class ClassName: |
| Middle | Attributes | self.x = value |
| Bottom | Methods | def method(self): |
Visibility
| UML | Meaning | Python Convention |
|---|---|---|
+ |
Public | self.name (no prefix) |
- |
Private | self.__name (double underscore) |
# |
Protected | self._name (single underscore) |
Types
| UML | Python |
|---|---|
name: str |
self.name: str = name |
get_price(): float |
def get_price(self) -> float: |
process(amount: float): bool |
def process(self, amount: float) -> bool: |
Relationships
| Symbol | Name | Meaning | Python Pattern |
|---|---|---|---|
| Inheritance | “is-a” — child extends parent | class Child(Parent): |
|
| Association | “knows-about” — stores a reference | self.other: OtherClass = other |
|
| Composition | “owns” — part destroyed with whole | self.part = Part(...) (created inside) |
|
| Aggregation | “uses” — part survives independently | self.parts.append(part) (passed in) |
|
| Dependency | “temporarily uses” — weakest link | Uses a class inside a method body only |
Dependency
A dependency is the weakest relationship between classes. It means one class temporarily uses another — typically as a method parameter or local variable inside a single method — without storing a persistent reference.
class ReportGenerator:
def generate(self, data: list) -> str:
formatter = HTMLFormatter() # Used locally, not stored
return formatter.format(data)
In UML, this is drawn as a dashed arrow from ReportGenerator to HTMLFormatter. The key difference from association: the ReportGenerator does NOT have an HTMLFormatter attribute — it only creates and uses one temporarily inside generate().
Rule of thumb:
self.x = OtherClass(...)→ association or composition (persistent reference)local_var = OtherClass(...)inside a method → dependency (temporary use)
Multiplicity
| Notation | Meaning |
|---|---|
1 |
Exactly one |
0..1 |
Zero or one (optional) |
* (preferred shorthand for zero or more) |
Zero or more |
1..* |
One or more |
n..m |
Between n and m |
Placement: the number sits next to the class it quantifies — it answers “for one of the opposite class, how many of this class?”
Style (Ambler G117): Show multiplicity on both ends of every relationship; prefer * over verbose 0..*.
Abstract Classes
| UML | Meaning | Python |
|---|---|---|
| Italic class name | Abstract class — cannot be instantiated | class Name(ABC): |
Italic method name / {abstract} |
Abstract method — must be overridden | @abstractmethod |
Choosing the Right Relationship — a Decision Flowchart
When you’re writing a class, ask these questions in order:
- Does this class’s
__init__create the other object internally, and the other object makes no sense outside this one? → Composition (e.g.,Invoice→LineItem) - Does a persistent
self.x: Otherstore an object that was created outside, and survives this object being destroyed? → Aggregation (e.g.,Team→Player) → If aggregation feels contested, a plain Association is always safer. - Is this class a kind of the other, sharing its interface and some behavior? → Inheritance (apply the “Is-a” test first)
- Does the class only mention the other inside a method body, with no persistent reference? → Dependency
If none of these apply, there is no relationship — don’t draw one.
What You Learned
UML class diagrams are a communication tool. They make invisible design decisions visible — turning implicit code relationships into explicit, communicable blueprints. You can now:
- Read a UML class diagram and understand its structure
- Write Python code that matches a given diagram
- Identify anti-patterns like the God Class
- Distinguish between association, composition, and aggregation
- Communicate software architecture without showing code
- Recognise the limits of UML — aggregation’s fuzzy semantics, the language-specific gap between Python’s
_/__and UML-/#, and when to leave notation off rather than force it
# This is the reference page — no coding task here.
# Review the summary above and use it as a quick reference!