UML Basics: Class Diagrams¶
What you will learn
- What UML is and which of its diagrams matter for low-level design.
- How to read and draw every part of a class diagram: the class box, visibility, and all six relationships (association, aggregation, composition, dependency, inheritance, realization).
- How each notation maps to real Python code, so you can go from diagram to code and back.
- How to choose the right arrow, how much detail to draw, and the mistakes that cost marks in interviews.
- The Mermaid syntax for each notation, so your diagrams live in Markdown and render on GitHub and on this site.
The Python snippets on this page were run on Python 3.12, and every diagram was checked with the Mermaid parser.
1. What UML is, and why it exists¶
Code is precise but slow to read. A picture of "which classes exist and how they are connected" can be understood in seconds, and it lets a team discuss a design before writing it.
The Unified Modeling Language (UML) is a standard visual vocabulary for this. It was unified in the 1990s from earlier notations by Grady Booch, Ivar Jacobson and James Rumbaugh, and is now maintained by the Object Management Group (OMG). The point of a standard is that a rectangle, a diamond or a dashed arrow means the same thing to everyone, so you can hand a diagram to a stranger and they read it the same way you drew it.
UML 2.x defines 14 diagram types, split into structure diagrams (what the parts are) and behaviour diagrams (what happens over time). In low-level design you will mostly use these:
| Diagram | Answers the question | Covered here |
|---|---|---|
| Class diagram | What classes exist, what do they hold and do, and how are they related? | Yes, in depth |
| Sequence diagram | In what order do objects call each other for one scenario? | Briefly, see section 9 |
| State diagram | What states can an object be in, and what moves it between them? | Briefly |
| Activity diagram | What are the steps and decisions in a process? | Briefly |
The class diagram is the most important one. It is the one you draw first in a low-level design interview.
2. Anatomy of a class¶
A class is drawn as a rectangle with up to three compartments:
classDiagram
class BankAccount {
-account_no : str
#balance : Decimal
+owner : str
+deposit(amount : Decimal) None
+withdraw(amount : Decimal) bool
+count()$ int
}
| Compartment | Contains | In the diagram above |
|---|---|---|
| Top: name | The class name | BankAccount |
| Middle: attributes | The data the class holds (fields) | account_no, balance, owner |
| Bottom: operations | What the class can do (methods) | deposit, withdraw, count |
Attribute and operation syntax¶
UML writes them like this:
attribute: visibility name : type [multiplicity] = default value
operation: visibility name(parameter : type, ...) : return type
Examples: - balance : Decimal = 0, + deposit(amount : Decimal) : None. Mermaid accepts this form. It also accepts a C-style type name order (+Decimal balance), which you will see in many online diagrams. Pick one style and use it consistently. This site uses the UML form name : type.
Visibility¶
The symbol in front of a member says who may use it:
| Symbol | Name | Meaning | Python equivalent |
|---|---|---|---|
+ |
Public | Any code may use it | name |
- |
Private | Only the class itself | __name (name-mangled) or, more commonly, _name |
# |
Protected | The class and its subclasses | _name by convention |
~ |
Package | Code in the same package | No direct equivalent. A module-level name is the closest |
Python has conventions, not enforcement
Python does not stop anyone from reading _balance. So in a diagram, - and # express design intent: "outside code should not touch this". That is still valuable, because it tells the reader what the public surface of a class is.
Static and abstract members¶
| Marker | Meaning | Drawn as | Python equivalent |
|---|---|---|---|
| Static | Belongs to the class, not to an instance | Underlined ($ after the member in Mermaid) |
Class attribute, @classmethod, @staticmethod |
| Abstract | Declared but not implemented, subclasses must provide it | Italic (* after the member in Mermaid) |
@abstractmethod |
A class with at least one abstract member is an abstract class: it cannot be instantiated. Mark it with the <<abstract>> annotation or an italic name.
Annotations (stereotypes)¶
Text in double angle brackets above the class name says what kind of thing it is:
| Annotation | Meaning | Typical Python |
|---|---|---|
<<interface>> |
A pure contract: operations with no implementation | typing.Protocol, or an ABC with only abstract methods |
<<abstract>> |
A class that cannot be instantiated on its own | ABC with @abstractmethod |
<<enumeration>> |
A fixed set of named values | enum.Enum |
<<dataclass>>, <<service>>, ... |
Your own labels for extra meaning | @dataclass, a plain class |
The same class in Python¶
Every part of the box maps to code:
from abc import ABC, abstractmethod
from decimal import Decimal
class Account(ABC): # <<abstract>>
_count: int = 0 # static attribute
def __init__(self, owner: str) -> None:
self.owner = owner # + owner : str
self._balance = Decimal(0) # # balance : Decimal
self.__pin = "0000" # - pin : str (name-mangled)
Account._count += 1
def deposit(self, amount: Decimal) -> None: # + deposit(amount : Decimal) : None
self._balance += amount
@staticmethod
def count() -> int: # + count() : int (static)
return Account._count
@abstractmethod
def monthly_fee(self) -> Decimal: # + monthly_fee() : Decimal (abstract)
...
class Savings(Account):
def monthly_fee(self) -> Decimal:
return Decimal(0)
account = Savings("Asha")
account.deposit(Decimal(100))
print(Account.count(), account.monthly_fee())
Output:
3. Relationships: the heart of a class diagram¶
Boxes tell you what exists. Lines tell you the design. There are six kinds of relationship, and choosing the right one is the skill.
| Relationship | Line and arrowhead | Read it as | Typical code shape |
|---|---|---|---|
| Association | Solid line, optional open arrowhead | "A knows about B" | A field that holds a B |
| Aggregation | Solid line, hollow diamond at the whole | "A has B, but B can live without A" | A field holding B objects that were created elsewhere |
| Composition | Solid line, filled diamond at the whole | "A owns B. B dies with A" | A creates B itself and nobody else holds it |
| Dependency | Dashed line, open arrowhead | "A uses B briefly" | B is a parameter, a return value, or a local variable |
| Generalization (inheritance) | Solid line, hollow triangle at the parent | "A is a kind of B" | class A(B) |
| Realization (implements) | Dashed line, hollow triangle at the interface | "A promises to provide what interface B declares" | class A(B) where B is an interface or Protocol |
Two habits make the arrows easy to remember:
- The arrowhead or diamond sits at the "more general" or "owning" end. Triangles point at the parent, diamonds sit on the whole, and plain arrowheads point at the thing being used.
- Dashed means weak or temporary. Dependency is dashed because it is a passing use, and realization is dashed because an interface is a promise, not a shared implementation.
classDiagram
direction LR
Animal <|-- Dog : generalization
Pet <|.. Dog : realization
Team "1" o-- "*" Player : aggregation
House "1" *-- "*" Room : composition
Order "*" --> "1" Customer : association
Printer ..> Invoice : dependency
All six relationships in one picture, each labelled with its name. The next sections take them one at a time.
3.1 Association: "knows about"¶
The most general relationship: one class holds a reference to another over time (not just during one call).
classDiagram
direction LR
class Order
class Customer
Order "*" --> "1" Customer : placed by
Read: many orders are placed by one customer. The arrowhead shows that an Order can navigate to its Customer.
- The label (
placed by) names the relationship. Write it as a phrase that reads correctly in the arrow's direction. - The arrowhead shows navigability: which side can reach the other.
Order --> Customermeans an order holds its customer, but a customer does not necessarily hold its orders. A plain line (--) means "related, direction not decided yet". - The numbers are multiplicities, explained in 3.2.
class Customer:
def __init__(self, name: str) -> None:
self.name = name
class Order:
def __init__(self, customer: Customer) -> None:
self.customer = customer # a long-lived reference: Order --> Customer
asha = Customer("Asha")
order = Order(asha)
print(order.customer.name)
Output:
When to use: whenever two classes collaborate for longer than one method call and neither "owns" the other. If you are unsure which of the more specific relationships applies, association is the safe default.
3.2 Multiplicity: how many?¶
Numbers at the ends of a line say how many objects take part:
| Notation | Meaning | Python type |
|---|---|---|
1 |
Exactly one | Customer |
0..1 |
Zero or one (optional) | Passport or None |
* or 0..* |
Zero or more | list[Order] |
1..* |
One or more | list[Line] with at least one item |
n (e.g. 3) |
Exactly n | A fixed-size sequence |
m..n (e.g. 2..5) |
Between m and n | A bounded list |
classDiagram
direction LR
Customer "1" --> "*" Order : places
Order "1" *-- "1..*" OrderLine : contains
Person "1" --> "0..1" Passport : holds
Read each end aloud: "one customer places zero or more orders", "one order contains one or more lines", "a person holds zero or one passport". Reading the multiplicity out loud is the best way to catch a wrong one.
from dataclasses import dataclass, field
@dataclass
class Passport:
number: str
@dataclass
class Order:
id: int
@dataclass
class Customer:
name: str
orders: list[Order] = field(default_factory=list) # 0..*
@dataclass
class Person:
name: str
passport: Passport | None = None # 0..1
person = Person("Asha")
print(person.passport)
person.passport = Passport("K1234567")
print(person.passport.number)
Output:
Interview tip
Always put multiplicities on associations. "One-to-many" versus "many-to-many" changes the code (a single field versus a collection, or a link class in the middle), and interviewers check that you noticed.
3.3 Aggregation: "has a, but can live on its own"¶
A whole made of parts, where the parts have their own life. Remove the whole and the parts still exist.
classDiagram
direction LR
Team "1" o-- "*" Player : has
Read: a team has many players. The hollow diamond is on the whole, Team. Players exist independently of any particular team.
class Player:
def __init__(self, name: str) -> None:
self.name = name
class Team:
def __init__(self, name: str) -> None:
self.name = name
self.players: list[Player] = []
def sign(self, player: Player) -> None:
self.players.append(player) # the player comes from outside
asha = Player("Asha")
team = Team("Red")
team.sign(asha)
del team # the team is gone...
print(asha.name) # ...but the player lives on
Output:
When to use: collections of independent things: a team and its players, a playlist and its songs, a department and its employees.
The fuzzy one
The UML specification gives aggregation very little precise meaning beyond "whole and part", and many authors recommend drawing plain association instead. Interviewers, however, often still ask you to distinguish aggregation from composition, so learn the difference in the next section.
3.4 Composition: "owns, and the part dies with it"¶
A stronger whole-part relationship. The whole creates and owns its parts, a part belongs to one whole at a time, and when the whole is destroyed, so are its parts.
classDiagram
direction LR
House "1" *-- "1..*" Room : has
Read: a house is made of one or more rooms. The filled diamond is on the whole, House. A room has no meaning outside its house.
class Engine:
def __init__(self) -> None:
self.running = False
def start(self) -> None:
self.running = True
class Car:
def __init__(self) -> None:
self._engine = Engine() # created here, owned here, never handed out
def start(self) -> None:
self._engine.start()
@property
def running(self) -> bool:
return self._engine.running
car = Car()
car.start()
print(car.running)
Output:
Python cannot enforce lifetimes
Nothing stops another object from keeping a reference to car._engine after the car is gone, because _engine is only private by convention. Composition in a diagram states the design intent: this object is created by the whole, belongs to it alone, and is not shared. The clue in the code is that the whole creates the part and does not accept it from outside.
The lifetime test. Ask: "If I delete the whole, should the part still make sense?" A Room without a House? No, so composition. A Player without a Team? Yes, so aggregation or association.
| Aggregation | Composition | |
|---|---|---|
| Diamond | Hollow | Filled |
| Who creates the part? | Someone else, then it is added | The whole itself |
| Part can be shared? | Yes | No, one owner |
| Part outlives the whole? | Yes | No |
| Example | Team and Player | House and Room |
3.5 Dependency: "uses briefly"¶
The weakest relationship: one class uses another only inside a method. It does not keep a reference.
classDiagram
direction LR
Printer ..> Invoice : prints
Read: a printer depends on an invoice. If Invoice changes, Printer may break, but Printer does not remember any invoice.
class Invoice:
def __init__(self, total: float) -> None:
self.total = total
class Printer:
def print(self, invoice: Invoice) -> str: # Invoice appears only as a parameter
return f"Invoice total: {invoice.total:.2f}"
print(Printer().print(Invoice(250)))
Output:
Signs of a dependency in code: the other class shows up as a parameter type, a return type, a local variable, or is only called statically. It is not stored in a field, otherwise it is an association.
When to use: to show that a change in B can ripple into A. Draw dependencies sparingly: most classes depend on many others, and showing all of them turns the diagram into noise. Show the ones that matter to the design.
3.6 Generalization: "is a kind of" (inheritance)¶
A child class is a specialised version of a parent and inherits its attributes and operations.
classDiagram
direction TB
class Animal {
+name : str
+eat() None
}
class Dog {
+bark() None
}
class Cat {
+purr() None
}
Animal <|-- Dog
Animal <|-- Cat
Read: a dog is an animal, and a cat is an animal. The hollow triangle points at the parent. Dog shows only what it adds (bark), because name and eat are inherited.
class Animal:
def __init__(self, name: str) -> None:
self.name = name
def eat(self) -> str:
return f"{self.name} is eating"
class Dog(Animal):
def bark(self) -> str:
return f"{self.name} says woof"
dog = Dog("Rex")
print(dog.eat())
print(dog.bark())
print(isinstance(dog, Animal))
Output:
The is-a test. Say the sentence: "A Dog is an Animal." If it sounds wrong ("A Car is an Engine"), it is not inheritance. That case is composition: a car has an engine.
Common mistake: inheritance for code reuse
Do not inherit just to reuse code. If the "is a" sentence is false or only true by accident, use composition (hold the other object in a field). Deep inheritance trees are hard to change, and this is why design pattern advice says "prefer composition over inheritance".
3.7 Realization: "implements this interface"¶
A class promises to provide what an interface declares. The interface has no implementation, only the contract.
classDiagram
direction TB
class PaymentMethod {
<<interface>>
+pay(amount : Decimal) bool
}
class CreditCard {
+pay(amount : Decimal) bool
}
class Wallet {
+pay(amount : Decimal) bool
}
PaymentMethod <|.. CreditCard
PaymentMethod <|.. Wallet
Read: CreditCard and Wallet both realize PaymentMethod. The line is dashed (a promise), and the triangle still points at what is being implemented. Callers hold a PaymentMethod and do not care which one.
from abc import ABC, abstractmethod
from decimal import Decimal
class PaymentMethod(ABC): # <<interface>>
@abstractmethod
def pay(self, amount: Decimal) -> bool: ...
class CreditCard(PaymentMethod):
def pay(self, amount: Decimal) -> bool:
return amount <= Decimal(5000) # pretend the card has a limit
class Wallet(PaymentMethod):
def pay(self, amount: Decimal) -> bool:
return amount <= Decimal(100)
methods: list[PaymentMethod] = [CreditCard(), Wallet()]
print([method.pay(Decimal(250)) for method in methods])
Output:
In Python you can also write the interface as a typing.Protocol. Then a class realizes it implicitly, just by having the right methods, without naming it in class X(...). Draw it the same way: the diagram shows the design, and the language decides how strictly it is enforced.
Some diagrams draw an interface as a lollipop: a small circle on a line. It says "this class provides this interface" without drawing the interface box. Use it when the interface has no interesting detail.
When to use realization: whenever callers should depend on a capability and not a concrete class. It is the notation behind most design patterns (Strategy, Factory, Observer, and others).
3.8 Which arrow do I draw?¶
Work through these questions in order:
flowchart TD
A[Two classes are related] --> B{Is one a kind of the other?}
B -- "yes, and the parent is an interface" --> R[Realization: dashed line, hollow triangle]
B -- "yes, and the parent is a class" --> G[Generalization: solid line, hollow triangle]
B -- no --> C{Does one keep a reference to the other in a field?}
C -- no --> D{Does one use the other inside a method?}
D -- yes --> DEP[Dependency: dashed arrow]
D -- no --> X[No relationship: leave it out]
C -- yes --> E{Does the whole create and own the part, so the part dies with it?}
E -- yes --> COMP[Composition: filled diamond]
E -- no --> F{Is it a collection of parts that live on their own?}
F -- yes --> AGG[Aggregation: hollow diamond]
F -- no --> ASSOC[Association: solid line or arrow]
When in doubt between two, pick the weaker one (dependency over association, association over aggregation). A wrong strong relationship misleads more than a missing one.
4. More notation you will meet¶
Enumerations¶
A fixed set of named values. Draw a class with the <<enumeration>> annotation and list the values.
classDiagram
class OrderStatus {
<<enumeration>>
PLACED
SHIPPED
DELIVERED
CANCELLED
}
Order --> OrderStatus : status
from enum import Enum
class OrderStatus(Enum):
PLACED = "placed"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
print(OrderStatus.SHIPPED.value)
Output:
Generic (template) classes¶
A class parameterised by a type. UML puts the type parameter in angle brackets, and Mermaid writes it between tildes (~T~).
classDiagram
class Stack~T~ {
-items : list~T~
+push(item : T) None
+pop() T
}
class Stack[T]: # PEP 695 syntax, Python 3.12
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
numbers = Stack[int]()
numbers.push(7)
print(numbers.pop())
Output:
Association class¶
Sometimes the relationship itself has data: a student enrols in a course, and the enrolment has a grade. UML draws a class attached by a dashed line to the association. Mermaid has no association-class notation, so the standard workaround is to draw the relationship as a class in the middle, which is also how you would write the code:
classDiagram
direction LR
class Enrollment {
+grade : str
}
Student "1" --> "*" Enrollment
Enrollment "*" --> "1" Course
from dataclasses import dataclass
@dataclass
class Student:
name: str
@dataclass
class Course:
title: str
@dataclass
class Enrollment: # the relationship, promoted to a class
student: Student
course: Course
grade: str
enrollment = Enrollment(Student("Asha"), Course("Algorithms"), grade="A")
print(enrollment.student.name, enrollment.course.title, enrollment.grade)
Output:
Self-association¶
A class related to itself, for example a manager who is also an employee:
classDiagram
direction LR
Employee "0..1" --> "*" Employee : manages
Read: an employee is managed by zero or one manager, and a manager manages zero or more employees.
Notes, constraints and packages¶
- A note is free text attached to a class. Use it for a rule the notation cannot express, such as "balance can never be negative".
- A constraint is a rule in braces, such as
{ordered}on a relationship, meaning the items keep an order. - A package (a namespace) groups related classes. It corresponds to a Python module or package.
classDiagram
namespace Billing {
class Invoice
class Payment
}
Invoice --> Payment
note for Payment "amount must be greater than zero"
5. Putting it together: a library system¶
A single diagram that uses most of the notation. Read it before looking at the explanation below it.
classDiagram
direction LR
class Person {
<<abstract>>
#name : str
+email()* str
}
class Member {
-member_id : int
+borrow(copy : BookCopy) Loan
}
class Librarian {
+register(member : Member) None
}
class Library {
-name : str
+find(title : str) list~Book~
}
class Book {
+isbn : str
+title : str
}
class BookCopy {
-barcode : str
+status : CopyStatus
}
class CopyStatus {
<<enumeration>>
AVAILABLE
ON_LOAN
LOST
}
class Loan {
+due_date : date
+is_overdue() bool
}
class Notifier {
<<interface>>
+send(member : Member, text : str) None
}
class EmailNotifier
Person <|-- Member
Person <|-- Librarian
Library "1" *-- "*" BookCopy : owns
Library "1" o-- "*" Member : registers
BookCopy "*" --> "1" Book : is a copy of
BookCopy --> CopyStatus
Member "1" --> "*" Loan : has
Loan "*" --> "1" BookCopy : covers
Notifier <|.. EmailNotifier
Loan ..> Notifier : reminds via
Read it line by line:
| Line | In words | Why this notation |
|---|---|---|
Member and Librarian each to Person (hollow triangle) |
A member is a person, and so is a librarian | Generalization: shared name and email() live in the abstract parent |
Library "1" *-- "*" BookCopy |
A library owns many physical copies, and a copy belongs to exactly one library | Composition: a copy is created by the library and is meaningless without it |
Library "1" o-- "*" Member |
A library has registered members | Aggregation: a person continues to exist if they leave the library |
BookCopy "*" --> "1" Book |
Many copies are copies of one book title | Association with multiplicity: each copy knows its title |
BookCopy --> CopyStatus |
A copy has a status | Association to an enumeration |
Member "1" --> "*" Loan and Loan "*" --> "1" BookCopy |
A member has loans, and each loan covers one copy | The Loan is an association class: the borrowing itself holds data (due_date) |
EmailNotifier to Notifier (dashed, hollow triangle) |
An email notifier implements the notifier contract | Realization: callers depend on the interface |
Loan ..> Notifier |
A loan uses a notifier to send reminders | Dependency: used inside a method, not stored |
Design decisions visible in the picture: Book (the title) and BookCopy (the physical item) are separate classes because "the library has 3 copies of one book" is a fact the model must express. Loan is its own class because it carries data and has behaviour (is_overdue). Notifier is an interface so the library does not care how reminders are sent.
Exercise: read the diagrams you have already met¶
Two earlier pages contain class diagrams. Try naming each relationship before checking the answer.
| Look for | Relationship | Why |
|---|---|---|
Logger and LogHandler: open diamond on Logger |
Aggregation | The logger has handlers that are created elsewhere and added to it |
Logger and LogRecord: dashed arrow |
Dependency | The logger creates a record inside log() and does not store it |
ConsoleHandler and LogHandler: hollow triangle at LogHandler |
Generalization | A console handler is a handler |
LogHandler and Formatter: solid arrow |
Association | The handler keeps a formatter in a field |
SimpleFormatter and Formatter: dashed line, hollow triangle |
Realization | Formatter is a Protocol, and SimpleFormatter implements it |
NotifierFactory and Notifier: dashed arrow |
Dependency | The factory builds a notifier and hands it over |
Notifier and Channel: dashed arrow |
Dependency | create_channel() returns a channel that notify() uses locally |
Channel and Gateway: solid arrow |
Association | The channel holds its gateway in a field |
6. How much detail to draw¶
A class diagram is a communication tool, not a full specification. Choose the level to match the audience:
| Level | Shows | Use it when |
|---|---|---|
| Conceptual | Class names and relationships only | Early discussion, or explaining the domain to non-programmers |
| Design | Key attributes and the important operations, visibility, multiplicities | Interviews and design reviews. This is the default |
| Implementation | Every attribute, signature and type | Documenting an existing codebase, usually generated by a tool |
For low-level design interviews, aim for the design level:
- Include: classes with a clear responsibility, the important operations, multiplicities, and every relationship that shapes the design.
- Leave out: getters and setters, trivial helpers,
__init__boilerplate, and dependencies that do not matter to the story you are telling.
A drawing routine that works¶
- List the nouns in the requirements. These are candidate classes.
- Give each class one responsibility. Split or merge until each has a single clear job.
- List the verbs. These become operations, on the class that has the data.
- Draw the relationships using the flowchart in 3.8, then add multiplicities.
- Look for variation. Where behaviour differs (payment types, notification channels), introduce an interface and realizations.
- Name the patterns you used, and add a note for any rule the notation cannot show.
7. Mermaid cheat sheet¶
Everything above, in the syntax this repository uses:
Class box class Name { +field : type -private #protected ~package }
Method +method(arg : type) return_type
Static / abstract +method()$ type +method()* type
Annotation <<interface>> <<abstract>> <<enumeration>> (first line inside the box)
Generic class Stack~T~ list~T~
Lollipop interface Pet ()-- Dog
Generalization Parent <|-- Child
Realization Interface <|.. Implementation
Composition Whole *-- Part
Aggregation Whole o-- Part
Association A --> B (plain link: A -- B)
Dependency A ..> B
Multiplicity Customer "1" --> "0..*" Order : places
Label A --> B : label text
Note note for Name "text"
Package namespace Billing { class Invoice }
Layout direction direction LR (LR, RL, TB, BT)
Mermaid draws diagrams from text, so they can live in the same Markdown file as the explanation, show up cleanly in code review diffs, and never go out of date as image files do. One limitation to remember: it has no association-class or qualified-association notation, and no use-case diagram type.
8. When to use class diagrams¶
Use them:
- In low-level design interviews, right after clarifying requirements.
- In design reviews and pull requests, to show how a change reshapes the classes.
- To onboard someone to a codebase, or to document a module's structure.
- To compare alternatives: draw both designs and see which is simpler.
- To spot problems early: a class with 12 arrows into it is a warning sign, and so is a diagram you cannot draw on one screen.
Do not use them:
- To describe behaviour over time. A class diagram is static. Use a sequence or state diagram for "what happens when".
- For every class in a large system. Draw the part relevant to the question.
- As a substitute for talking. A diagram nobody explains is only half a design.
| Benefit | Cost |
|---|---|
| A whole design becomes visible in seconds | Static only: it shows structure, not the order of calls |
| Forces you to decide relationships and multiplicities | Drawings go stale when the code changes, unless they are generated or kept in the repo |
| A shared vocabulary within a team | Too much detail makes them unreadable, and too little makes them useless |
| Cheap to change before any code exists | The notation has subtle rules (aggregation vs composition) that people get wrong |
Real world
Some tools go the other direction, and generate class diagrams from code. For Python, pylint ships pyreverse, which reads a package and draws its classes and relationships. That is handy for documenting an existing codebase, though hand-drawn diagrams at the design level are usually clearer than generated implementation-level ones.
9. Other UML diagrams you will meet¶
Class diagrams show structure. When the question is about behaviour, switch diagram. All of these render in Mermaid except the use-case diagram.
| Diagram | Shows | Use it when | Mermaid type |
|---|---|---|---|
| Sequence | Messages between objects in time order for one scenario | Explaining a flow such as "what happens on log.info()" |
sequenceDiagram |
| State | The states of one object and the events that move it between them | An object with a lifecycle: an order (placed, shipped, delivered), a connection | stateDiagram-v2 |
| Activity | Steps, decisions and parallel branches of a process | A workflow or an algorithm | flowchart |
| Use case | Who uses the system and for what goals | Gathering requirements with stakeholders | Not supported. Use a table of actors and goals |
| Entity-relationship | Tables and their relationships (not UML, but often drawn beside it) | Data modelling for a database | erDiagram |
The Logger and Notification Service pages use a class diagram for structure and a sequence diagram for the main flow, which is the usual pair for a low-level design answer.
10. Common mistakes¶
| Mistake | What is wrong | Fix |
|---|---|---|
| Arrow on the wrong end of inheritance | The triangle must point at the parent | Remember "child points to parent", and in Mermaid write the parent on the left of the arrow |
| Diamond on the part, not the whole | The diamond sits on the whole | Whole *-- Part |
| Everything is an association | The diagram loses the ownership and lifetime story | Use the flowchart in 3.8 |
| Composition when the part is shared | A composed part has exactly one owner | Use aggregation or association |
| Inheritance to reuse code | "Is a" is false, so the design is fragile | Hold the object in a field (composition) |
| No multiplicities | Readers cannot tell one-to-one from one-to-many | Put a number on every association end |
| Showing every getter, setter and dependency | The important structure is buried | Design-level detail only |
| One giant diagram | Nobody can read it | Split by feature or package, and keep each under about a dozen classes |
| Attributes that are really relationships | Order lists customer : Customer as an attribute and draws an arrow to Customer |
Show it once, as the relationship. Keep plain attributes for simple values |
Class names that are verbs or vague (Manager, Processor, Data) |
Unclear responsibility | Name classes after domain nouns |
11. Interview tips¶
Interview tip: what a good class diagram in an interview looks like
- Start from the requirements, and name classes with clear single responsibilities.
- Draw interfaces and abstract classes where behaviour varies, and say which pattern that is.
- Put multiplicities on associations and get the arrow directions right.
- Keep it to about 6 to 10 classes, with the key operations only.
- Talk while you draw, and state assumptions ("I assume a copy belongs to exactly one library").
Common questions and short model answers:
| Question | Short answer |
|---|---|
| Aggregation vs composition? | Both are "has-a". In composition the whole creates and owns the part, the part has one owner and dies with the whole (house and rooms). In aggregation the parts have their own lifetime and can be shared or moved (team and players) |
| Association vs dependency? | Association is a long-lived reference held in a field. Dependency is a short-lived use inside a method: a parameter, a return value or a local variable |
| Inheritance vs composition? | Inheritance is "is a" and fixes the relationship at design time. Composition is "has a" and lets you swap parts at run time. Prefer composition unless the "is a" sentence is truly correct |
| How do you show an interface? | A box with <<interface>> and realization arrows (dashed line, hollow triangle) from the implementing classes. In Python that is a Protocol or an ABC |
| What does the arrowhead on an association mean? | Navigability: the class at the tail can reach the class at the head. No arrowhead means it is undecided or bidirectional |
| How do you show a many-to-many relationship? | Two * multiplicities. If the relationship carries data, promote it to its own class (an association class) with a one-to-many on each side |
| How detailed should the diagram be? | Design level: important attributes and operations, no getters or setters, all the relationships that shape the design |
| Which other diagrams do you draw? | A sequence diagram for the main flow, and a state diagram if an object has a meaningful lifecycle |
| How do UML notations map to Python? | Generalization is class B(A). Realization is an ABC or Protocol. Composition is a part created inside __init__. Aggregation and association are injected fields. Dependency is a parameter or local. Enumeration is Enum. Generics are Stack[T] |
12. Key takeaways¶
- A class box has three compartments: name, attributes, operations. Members carry visibility (
+ - # ~), and static or abstract markers. - There are six relationships: association, aggregation, composition, dependency, generalization and realization. Choose by asking "is a?", "keeps a reference?", "owns and controls the lifetime?".
- Solid means strong, dashed means weak or a promise. Triangles point at the parent or interface, and diamonds sit on the whole.
- Composition = owns and dies with it. Aggregation = has, but the part can live on. Dependency = uses briefly.
- Always add multiplicities, and read them aloud to check them.
- Draw at the design level: the structure that matters, not every getter.
- A class diagram shows structure only. Pair it with a sequence or state diagram for behaviour.
- In Python, the notation states intent that the language does not enforce. That is exactly why it is worth drawing.
References¶
- Object Management Group. Unified Modeling Language (UML) Specification, version 2.5.1. The authoritative definition of the notation.
- Martin Fowler. UML Distilled: A Brief Guide to the Standard Object Modeling Language, 3rd edition. Addison-Wesley, 2003. A short, practical guide to the parts of UML worth using.
- Mermaid documentation: Class diagrams.
- Python documentation:
abc,typing.Protocol,enum,dataclasses, generic classes (PEP 695).