UML Sequence Diagrams in Python
Learn to read and create UML sequence diagrams by writing Python code that matches target diagrams. Starting from a few objects exchanging messages and building up to branches and loops, you will see how sequence diagrams turn invisible runtime behavior into a picture.
Your First Sequence Diagram
Why this matters
Class diagrams show what exists in a system; sequence diagrams show what happens at runtime — which object calls which method, in what order. As soon as you start designing or debugging real interactions (logins, API handshakes, message flows), you need a way to describe behavior over time, not just structure. This first step gives you the smallest complete sequence diagram and shows you how Python code on the page becomes a picture you can read.
🎯 You will learn to
- Apply the lifeline notation by identifying participants in a sequence diagram
- Create Python code that produces synchronous messages between two object instances
Where Class Diagrams End, Sequence Diagrams Begin
You already know class diagrams — they show what exists: classes, attributes, methods, relationships. A sequence diagram shows what happens at runtime: which object calls which method, and in what order.
Think of it as the difference between a floor plan (class diagram) and a security camera recording (sequence diagram). Same building, very different question.
Four Pieces of Notation
| Element | What it looks like | What it means |
|---|---|---|
| Participant (lifeline) | A box at the top, with a dashed line below | A specific object instance active during the scenario |
| Synchronous message | Solid arrow with a filled arrowhead → | One object calls a method on another, and waits for it to finish |
| Activation box | A thin rectangle on the lifeline | The object is currently executing — a call stack frame in memory |
| Time | Top-to-bottom | Earlier events are higher up; later events are lower |
Key distinction: A lifeline is not a class.
bot: DiscordBotmeans “this particular bot instance”. If your code creates two bots, you get two lifelines — even though there is only oneDiscordBotclass.
A Simpler Example First
Here is a minimal diagram — a user object calls login() on an auth object:
Two lifelines, one synchronous call. That is a complete sequence diagram. Read the arrow as a sentence: “user calls login(password) on auth, and waits for it to finish.”
Your Target Diagram
Now let us build one together. Write Python code until the live Sequence Diagram panel matches this target:
Reading the target:
Mainis the script itself — any code outside a class or function (specifically, the body ofif __name__ == "__main__":) becomes a synthetic lifeline labeled Main. You didn’t declare it; the analyzer did, to represent “whoever is starting the scenario.”bot: DiscordBotis a specific bot instance created bybot = DiscordBot()channel: Channelis a specific channel instance- The two dashed
<<create>>arrows appear becauseMainconstructs each object - The two solid arrows are synchronous calls —
Maincallssend(...)onbot, thennotify_members(...)onchannel
Note —
Mainis a learning scaffold, not real-world practice. In this tutorial every diagram starts from__main__, giving you a concrete Python anchor for every arrow. Professional sequence diagrams almost never do this. A real diagram focuses on a specific interaction between objects that are already alive — it picks up the story at an interesting method call and does not trace from program startup. You would not see aMainlifeline in a diagram drawn on a whiteboard during a design meeting; instead you might seeuser,authService, anddatabase— all assumed to exist — with the scenario beginning atuser -> authService: login(password). TheMainlifeline is here purely to make Python execution explicit while you are learning the notation.
Your Task
The file step1/chatbot.py already defines DiscordBot and Channel. Your job is to write the if __name__ == "__main__": block so it:
- Creates a
DiscordBotinstance calledbot - Creates a
Channelinstance calledchannel - Calls
bot.send("Hello, world!") - Calls
channel.notify_members("Welcome")
Watch the Sequence Diagram panel — it updates live as you type!
Heads up: Variable names become participant names. If you write
dbot = DiscordBot()instead ofbot = DiscordBot(), the diagram will showdbot: DiscordBot. Pick meaningful names — they end up in the picture.
class DiscordBot:
def send(self, message):
print(f"[BOT] {message}")
class Channel:
def notify_members(self, message):
print(f"[CHANNEL] {message}")
if __name__ == "__main__":
# Your task: make the diagram match the target.
#
# 1. Create a DiscordBot called `bot`
# 2. Create a Channel called `channel`
# 3. Call bot.send("Hello, world!")
# 4. Call channel.notify_members("Welcome")
pass
Step 1 — Knowledge Check
Min. score: 80%1. In a sequence diagram, what does a single lifeline represent?
A lifeline represents one object instance, not a class. If your code does a = Dog() and b = Dog(), you get two lifelines (a: Dog and b: Dog) even though there is only one Dog class. This is the single most common misconception when switching from class diagrams to sequence diagrams.
2. What does a solid arrow with a filled arrowhead (→) mean?
A solid line with a filled arrowhead is a synchronous message — a normal method call where the caller blocks until the callee returns. This matches Python’s default behavior: every x.method() call waits for method() to finish before the next line executes.
3. Predict before you look. Given this Python __main__ block, how many lifelines will the sequence diagram show (including Main)?
if __name__ == "__main__":
a = DiscordBot()
b = DiscordBot()
c = Channel()
a.send("hi")
Four lifelines. Main, plus one for each object that gets created: a: DiscordBot, b: DiscordBot, c: Channel. Even though a and b are the same class, each instance gets its own lifeline. This is the lifelines-are-instances rule in action.
4. In a sequence diagram, how is time represented?
Top to bottom. The horizontal axis shows who is involved (the lifelines); the vertical axis shows when. This means the order of your Python statements directly controls the vertical order of the arrows.
Return Values: The Dashed Arrow
Why this matters
Most useful methods give something back — a count, a status, a result — and the diagram has to show those returns without burying the reader in noise. UML draws a dashed return arrow only when the returned value carries information the reader cares about, so you need to recognise the two precise conditions that trigger one. Get this right and your diagrams stay readable; miss it and either important data disappears or trivial returns clutter the picture.
🎯 You will learn to
- Analyze when a return message appears on a sequence diagram (and when it does not)
- Apply Python type annotations and assignments to produce a dashed return arrow
The Two Rules for Return Arrows
A return message is drawn as a dashed arrow with an open arrowhead (⇠). It points back from the callee to the caller, at the moment the method finishes.
But here is the catch — sequence diagrams do not draw a return arrow for every call. That would be noise. Instead, two things must be true:
- The method has a non-
Nonereturn type (annotate it:-> int,-> str, etc.) - The caller captures the return value in a variable (
count = bot.get_count())
If you just write bot.send("hi") and ignore any return, no dashed arrow appears — because “the call finished and came back” is already implied by the activation box ending. UML only shows returns when they carry information the reader cares about.
Example — With and Without Capture
Without capture — a solid call and an activation box, but no dashed return:
With capture — solid arrow going in, dashed arrow coming back:
Read the dashed arrow as “the method finished and handed back a value of this type.”
Your Target Diagram
Extend the chat bot from Step 1. Now DiscordBot has a method that reports the current member count, and Main captures it to decide what to say:
Notice the new dashed arrow from bot back to Main labeled int — that is the return arrow. The old call to channel.notify_members(...) has no dashed return arrow because its return type is None.
Your Task
Open step2/chatbot.py. The starter code has the method defined, but the __main__ block:
- Does not capture the return value of
get_member_count()— fix that - Uses a hardcoded string — replace it with an f-string that uses the captured count
Reminder: For the dashed arrow to appear, two things must be true — the method must have a return type annotation (
-> intalready in the starter), and you must assign the return value to a variable.
class DiscordBot:
def send(self, message: str) -> None:
print(f"[BOT] {message}")
def get_member_count(self) -> int:
return 5
class Channel:
def notify_members(self, message: str) -> None:
print(f"[CHANNEL] {message}")
if __name__ == "__main__":
bot = DiscordBot()
channel = Channel()
# TODO: capture the return value of bot.get_member_count()
bot.get_member_count()
# TODO: use the captured count in the notify message
channel.notify_members("5 members online")
Step 2 — Knowledge Check
Min. score: 80%1. What does a dashed arrow with an open arrowhead mean in a sequence diagram?
Dashed line + open arrowhead = return message. Solid line + filled arrowhead = synchronous call. The two visually distinct styles let you see “went in” vs. “came out” at a glance.
2. Why does this call NOT produce a return arrow on the diagram, even though it is syntactically a Python call?
bot.send("Hello")
The diagram draws a return arrow only when the return type is not None and the return value is captured. send returns None (no -> int or similar annotation), so there is no “value” to show on the way back — the end of the activation box is enough.
3. Predict. Which of these Python snippets produces a dashed return arrow?
# A
bot.get_member_count()
# B
count = bot.get_member_count() # get_member_count is annotated `-> int`
# C
x = bot.send("hi") # send is annotated `-> None`
Only B. A calls the method but throws the return value away, so no arrow. C captures the return, but -> None means there is no meaningful value to show. B is the one that ticks both boxes — non-None return type and captured value.
4. In Python, self is the first parameter of every instance method. How is self drawn in a sequence diagram?
self is implicit in the diagram — a lifeline is the object, so there is no need to draw self separately. You will see self again in the next step when an object calls one of its own methods — that is when the lifeline points an arrow at itself.
Self-Calls and Nested Activation
Why this matters
Real classes rarely expose every detail; they delegate to private helper methods on the same object. When the diagram captures that delegation, you can see at a glance which public method is the orchestrator and which are its internal pieces. Activation boxes are not decoration — they are the literal call stack you already debug every day, drawn vertically. Connecting that mental model to the diagram is the threshold concept of this step.
🎯 You will learn to
- Analyze why an activation box represents a call stack frame
- Apply self-message notation to produce nested activation from Python code
The Call Stack, Drawn
You already know the call stack from debugging Python: every time a function calls another function, a new stack frame is pushed; when the function returns, the frame is popped.
A sequence diagram’s activation box is the exact visual of that. When a message arrives at a lifeline, an activation box starts. When the method returns, the box ends.
Mental model: Activation box ≈ stack frame. A method that takes longer has a taller box. A method that calls another method has a nested box stacked on top of its own. (The mapping is close but not perfect — generators, async, and coroutines blur the picture. For 99% of the synchronous code you will write as an undergraduate, “stack frame” is the right intuition.)
Self-Messages
When an object calls a method on itself (self.some_method()), the arrow loops back to the same lifeline — and a new activation box stacks on top of the current one. This is exactly how your Python interpreter works: a recursive or internal call pushes a fresh frame.
Example — A Method That Delegates
Consider an Order object whose checkout() method calls its own _validate() helper:
Notice the arrow from order to itself, and how it sits inside the outer activation box for checkout(). The small nested box is the stack frame for _validate() pushed on top of checkout()’s frame.
Your Target Diagram
In step3/chatbot.py, handle_message() should be a small orchestrator: it calls self._log() and then self.send(), both methods on the same bot. Your target:
Three arrows — one from Main to bot, and two from bot to itself. Visually, the two self-calls are nested inside the handle_message activation box because they happen while that method is still running.
Your Task
The starter file defines DiscordBot with _log() and send() methods, but handle_message() is empty. Your job:
- Fill in
handle_message()so it callsself._log(message)and thenself.send(message) - In
__main__, callbot.handle_message("hi there")— and only that
Watch for this:
self._log(...)— not_log(...)without theself.prefix. Withoutself., the call goes to a free function, not a method, and the sequence diagram will not draw the self-arrow. Theself.is what tells the analyzer “same object.”
class DiscordBot:
def _log(self, message: str) -> None:
print(f"[LOG] received: {message}")
def send(self, message: str) -> None:
print(f"[BOT] {message}")
def handle_message(self, message: str) -> None:
# TODO: inside this method, call self._log(message)
# and then self.send(message).
# Both calls should appear as self-arrows in the diagram.
pass
if __name__ == "__main__":
bot = DiscordBot()
# TODO: call bot.handle_message("hi there")
Step 3 — Knowledge Check
Min. score: 80%1. What does a nested activation box (a smaller box stacked on top of a larger one) represent?
A nested activation is the visual of the Python call stack: a method calls another method before returning, so a new frame is pushed on top. When the inner method returns, the inner box ends; when the outer returns, the outer box ends.
2. Which line of Python produces a self-arrow (an arrow from a lifeline back to itself)?
self.<method>(...) is what the analyzer recognizes as “same object.” The self. prefix matters — without it, the call would not be recognized as a method on the current object.
3. Predict. Given this code, how many arrows appear in the diagram?
class Bot:
def a(self): self.b()
def b(self): pass
if __name__ == "__main__":
bot = Bot()
bot.a()
Three arrows. (1) The <<create>> dashed arrow when bot = Bot(). (2) Main -> bot: a() for the outer call. (3) bot -> bot: b() for the self-call inside a(). The pass in b() is an empty body, so no further arrows come from there.
4. Review of Step 2. Suppose b() had been annotated def b(self) -> int: and a() had written x = self.b(). How many arrows would the diagram now show?
Trick question — and a useful one. The current analyzer draws return arrows only across different lifelines. A self-call returning to itself visibly starts and ends via the nested activation box, so no separate dashed arrow is drawn. This is why Step 2’s return-arrow examples always had the caller and callee on different lifelines. The “two rules” from Step 2 still hold, but there is a third, implicit rule: “caller ≠ callee.”
Conditional Fragments: opt and alt
Why this matters
Real behavior almost always branches — spam vs. legitimate traffic, cache hit vs. miss, authorised vs. denied. A sequence diagram that only shows a single straight-line trace cannot communicate any of that. The opt and alt interaction fragments are how UML draws conditional execution, and the only difference between them is whether there is an else. Mastering this small contrast lets you turn any Python if statement into the right diagram on the first try.
🎯 You will learn to
- Analyze when to choose
optvs.altbased on the Python control flow - Apply
ifandif/elseto produce each fragment in a sequence diagram
Combined Fragments Are Boxes Around Messages
So far every diagram has been a straight top-to-bottom trace. But real systems branch — sometimes they do X, other times Y. UML handles this with combined fragments: labeled boxes drawn around the messages they contain.
There are two conditional fragment types, and the only difference between them is whether there’s an else:
| Fragment | Label | Python | Meaning |
|---|---|---|---|
| opt | opt |
if ... (no else) |
Zero or one execution — inside runs only if the guard is true |
| alt | alt / else |
if ... else ... |
Exactly one branch runs — the guard selects which |
Both fragments wrap their region of the diagram in a thin rectangle with a guard condition (the Boolean test) in square brackets in the top-left corner.
Example — An opt Fragment
A bot decides whether to welcome a new member — only if they are not already subscribed. If they are subscribed, nothing happens:
The opt box says: “either this message happens, or nothing does — depending on the guard.” There is no second compartment.
Example — An alt Fragment
A spam filter: if spam, block; otherwise, forward. Two compartments, exactly one runs:
The alt box says: “exactly one of these branches runs.” The guard tells you which.
The choice rule:
optfor a single conditional message,altfor mutually-exclusive branches. If yourelsewould be empty, useopt; if both branches do something, usealt. The Python code shape decides for you — which is another reason to keep code and diagram in sync.
Your Target Diagram
The bot has a handle(channel, message) method that:
- If the message is spam: blocks it via
self._block(message). - Otherwise: forwards it to the channel via
channel.broadcast(message).
That’s a two-way split — an alt.
Your Task
The starter code has handle(channel, message) written with no branching — it unconditionally forwards everything. Your job:
- Replace the body with
if self._is_spam(message):/else:— produces thealtfragment with two compartments. - In the
ifbranch: callself._block(message). - In the
elsebranch: callchannel.broadcast(message).
Note on
_is_spam: It is already defined — a trivial classifier. You just need to call it in theifcondition. That call itself draws a tiny self-arrow (it’s a real method call) — that is expected.
class Channel:
def broadcast(self, message: str) -> None:
print(f"[CHANNEL] {message}")
class DiscordBot:
def _is_spam(self, message: str) -> bool:
return "buy now" in message.lower()
def _block(self, message: str) -> None:
print(f"[BLOCKED] {message}")
def handle(self, channel: Channel, message: str) -> None:
# TODO: rewrite this method so:
# - if self._is_spam(message): self._block(message)
# - else: channel.broadcast(message)
# That produces the `alt` fragment in the target diagram.
channel.broadcast(message)
if __name__ == "__main__":
bot = DiscordBot()
channel = Channel()
bot.handle(channel, "buy now cheap")
Step 4 — Knowledge Check
Min. score: 80%1. An alt fragment on a sequence diagram represents what Python construct?
alt is the conditional fragment — one compartment per branch, separated by horizontal lines, with exactly one compartment executing based on its guard. It maps directly to Python’s if / elif / else.
2. You wrote if user.is_new: bot.send_welcome(user) with no else. Which fragment appears on the diagram?
opt is the fragment for “maybe run this; maybe not.” It has one compartment. alt is for mutually-exclusive branches (two or more compartments). The only thing that changes between them is whether you wrote else.
3. Review of Step 3. The _is_spam call in the guard produces a tiny self-arrow before the alt box’s contents. Why does a self-arrow appear there at all?
The guard self._is_spam(message) is a real Python method call — the activation box for it is stacked on top of handle’s activation box, exactly like any other self-call from Step 3. Some published diagrams hide guard-evaluation calls to reduce clutter, but UML semantics say they are there.
Loops: Doing the Same Thing Many Times
Why this matters
Iteration is in nearly every real interaction — broadcasting to every subscriber, processing each item in a queue, retrying until success. A sequence diagram cannot duplicate the same arrow ten times to mean “this happens for every item”; it uses the loop fragment instead. The visual grammar is identical to opt and alt from Step 4 — a thin rectangle, a keyword, a guard in square brackets — only the meaning changes from pick to repeat. Once you see that pattern, you will recognise every fragment on sight.
🎯 You will learn to
- Apply
forandwhileloops in Python to produce aloopfragment in the diagram - Analyze when the right answer is one fragment vs. multiple smaller diagrams
The loop Fragment
Step 4 taught the two branching fragments (opt and alt). There is one more fragment you will use constantly: loop, for iteration.
| Fragment | Label | Python | Meaning |
|---|---|---|---|
| loop | loop |
for / while |
Contents run zero or more times |
The visual grammar is identical to opt and alt — a thin rectangle, a keyword in the top-left, a guard in square brackets. The only thing that changes is the keyword and the meaning: repeat instead of pick.
Example — A loop Fragment
Sending a welcome to every member — the message is sent once per iteration:
The loop box says: “the message(s) inside run once for every item in the collection.” If the collection is empty, the box still appears, but the messages inside run zero times.
Your Target Diagram
The bot has a broadcast_all(channel, messages) method that sends each message in the list to the channel.
Your Task (Fixer-Upper)
The starter code has broadcast_all written as a flat sequence — one unconditional call. That produces one bare arrow in the diagram. Your job:
- Replace the single call with
for msg in messages:— produces theloopfragment. - Inside the loop, call
channel.send_to_all(msg)once per iteration.
class Channel:
def send_to_all(self, message: str) -> None:
print(f"[CHANNEL] {message}")
class DiscordBot:
def broadcast_all(self, channel: Channel, messages: list) -> None:
# TODO: replace this unconditional call with a loop so the
# diagram shows a `loop` fragment instead of a single arrow.
channel.send_to_all(messages[0])
if __name__ == "__main__":
bot = DiscordBot()
channel = Channel()
bot.broadcast_all(channel, ["hi", "hello", "good morning"])
Step 5 — Knowledge Check
Min. score: 80%1. A loop fragment on a sequence diagram represents what Python construct?
loop wraps messages that repeat. It maps to Python’s for and while. The guard can describe the iteration (e.g., [for each message]).
2. Review of Step 4. Your method body is for x in items: if x.valid: bot.send(x). Which two fragments appear, and in what order?
The outer construct in Python is for, so the outer box is loop. Inside, the if without else produces opt. Fragment nesting mirrors the nesting of your Python code — read the indentation to predict the diagram.
3. You have this (made-up) diagram nesting:
loop
alt
opt
alt
...
end
end
end
end
Deeply nested fragments become unreadable fast. Ambler’s UML Style rule of thumb: if you are past two levels of nesting, split the diagram. Sequence diagrams are for communicating behavior, not for encoding every branch of your code.
4. A sequence diagram should typically focus on one scenario at a time. Which is the better choice?
Multiple small, focused diagrams. Each one answers a single question: “What happens when a valid user logs in?” or “What happens when payment fails?” This is a direct application of the Single Responsibility Principle to your diagrams.
Putting It All Together: A Moderated Broadcast
Why this matters
A real sequence diagram is never one notation in isolation — it weaves lifelines, returns, self-calls, and control-flow fragments into a single scenario that tells a story. You have learned every piece already; the difficulty here is integrating them. If you stare at the target diagram for a minute before seeing how it maps to code, that is the point — working developers have the same experience when they first design a real diagram, and the only way to build that fluency is to do it.
🎯 You will learn to
- Create a Python method whose sequence diagram combines lifelines, a captured return, self-calls, and both
altandloopfragments - Analyze a target diagram and predict its code shape before writing a line
The Scenario
The bot runs a daily digest over a list of recent posts. Before the loop starts, it asks the channel how many subscribers it has, so it can log the size of the digest. Then, for each post:
- Announcements (posts starting with
@all) get broadcast to the channel. - Everything else is silently skipped — the bot logs the skip but does not bother the channel.
Your Target Diagram
Notice every concept from Steps 1-5 appears:
- Lifelines and creation (Step 1):
Main,bot: DiscordBot,channel: Channel, with two<<create>>arrows. - Return value (Step 2): the dashed arrow labeled
count: intfromchannelback tobotafterget_subscriber_count()— the generator includes the bound variable name becausecountis used on the next line. - Self-call with nested activation (Step 3):
bot -> bot: _log_startand, inside the loop,bot -> bot: _log_skip. - Conditional fragment (Step 4): one
altinside the loop. - Loop fragment (Step 5): one outer
loopoverposts.
One loop outside, one alt inside — exactly the two-level nesting limit that Step 5’s quiz warned you not to exceed.
Your Task
Open step6/chatbot.py. The helper methods are already defined (Channel.get_subscriber_count, _is_announcement, _log_start, _log_skip). Your job is to:
- Implement
run_digest(channel, posts)onDiscordBotso it:- Captures the result of
channel.get_subscriber_count()in a local variable. - Calls
self._log_start(<that variable>)to announce the digest. - Iterates over
posts. For eachpost:- If
self._is_announcement(post): callchannel.broadcast(post). - Otherwise: call
self._log_skip(post).
- If
- Captures the result of
- In
__main__, create one bot, one channel, and callbot.run_digest(channel, posts)exactly once.
Predict first. Before you start typing, take 30 seconds and mentally walk the diagram: how many lifelines, how many arrows, which are dashed, where does the
altsit relative to theloop? Writing the code after visualising it is much faster than writing code and hoping the diagram matches.
class Channel:
def broadcast(self, message: str) -> None:
print(f"[BROADCAST] {message}")
def get_subscriber_count(self) -> int:
return 42
class DiscordBot:
def _is_announcement(self, post: str) -> bool:
return post.startswith("@all")
def _log_start(self, count: int) -> None:
print(f"[DIGEST] starting for {count} subscribers")
def _log_skip(self, post: str) -> None:
print(f"[DIGEST] skipped: {post}")
def run_digest(self, channel: Channel, posts: list) -> None:
# TODO: implement this method so it matches the target diagram.
# 1. Capture channel.get_subscriber_count() in a local variable
# 2. Call self._log_start(<that variable>)
# 3. for post in posts:
# if self._is_announcement(post):
# channel.broadcast(post)
# else:
# self._log_skip(post)
pass
if __name__ == "__main__":
posts = [
"@all staff meeting at 3pm",
"just saying hi",
"@all remember to stretch",
]
# TODO: create `bot` and `channel`, then call
# bot.run_digest(channel, posts) exactly once.
Step 6 — Knowledge Check
Min. score: 80%
1. Review of Step 1. Your diagram shows three lifelines: Main, bot: DiscordBot, and channel: Channel. If you changed __main__ to create two bots and one channel, how many lifelines would the diagram show (including Main)?
Lifelines are instances, not classes. Two DiscordBot() calls produce two distinct lifelines, plus Main and channel — four in total. This is the same rule from Step 1; it still applies no matter how complex the rest of the diagram is.
2. Review of Step 2. Why does the channel.get_subscriber_count() call produce a dashed return arrow, while the channel.broadcast(post) call does not?
Step 2’s two rules: the return type must be non-None and the caller must capture the value. get_subscriber_count meets both (-> int + count = ...); broadcast fails the first (-> None).
3. Review of Step 3. Why do self._log_start(count) and self._log_skip(post) appear nested inside the activation box for run_digest?
Activation boxes are stack frames. run_digest has not returned when it calls _log_start or _log_skip, so new frames are pushed on top of run_digest’s frame. This is Step 3’s call-stack intuition, unchanged.
4. Review of Steps 4 & 5. The target has a loop fragment containing an alt fragment. What Python control-flow structure produces this layout?
The outer box is loop (a for) and the inner box is alt (an if/else with both branches non-empty). Python indentation = fragment nesting: whichever block is innermost in the code is innermost in the diagram.
5. Design judgment. You want to extend this scenario to also handle a “hold the post for moderator review” case. Which is the better choice?
Sequence diagrams are for one scenario at a time. If you keep adding branches, you get the unreadable nested-fragment mess Step 5’s quiz warned about. Splitting into multiple small diagrams is not a failure — it is the correct application of the Single Responsibility Principle to your diagrams.
Sequence Diagram Reference
Why this matters
Congratulations — you can now read and write basic UML sequence diagrams: lifelines, synchronous calls, return messages, self-calls with nested activation, and the opt / alt / loop fragments. Step 6 proved you can weave them together in one scenario. The notation only sticks if you can pull it back out of memory later, so this page is structured as a self-test first and a cheat sheet second — retrieval before review is what makes the learning durable.
🎯 You will learn to
- Evaluate your own recall of every notation element introduced in Steps 1–6
- Apply this reference card as a quick lookup when designing future diagrams
Self-check (close this page first)
Before you scroll to the tables below, try to answer these from memory. Look back only when you are stuck:
- What does a lifeline represent — a class, an instance, or a file?
- What two conditions must BOTH be true for a dashed return arrow to appear?
- Why does a self-call produce a nested activation box?
- If your Python method is
for x in xs: if x.valid: bot.send(x)(noelse), what two fragments appear — and in which order?
Retrieval before review is the learning — just reading the tables again is not.
The Core Pieces
| Element | Looks like | Python that produces it |
|---|---|---|
| Lifeline | box on top, dashed line below | any object instance: bot = DiscordBot() |
| Activation box | thin rectangle on the lifeline | a method call — begins when the call arrives, ends when it returns |
| Synchronous message | solid line, filled arrowhead → | x.method(...) — caller waits |
| Return message | dashed line, open arrowhead ⇠ | y = x.method() and method returns a non-None type and caller ≠ callee |
| Self-message | arrow looping back to the same lifeline | self.method(...) inside a method |
| Creation | dashed arrow with <<create>> label to a new lifeline |
constructor: bot = DiscordBot() |
The Three Fragments You Will Use Most
| Fragment | Meaning | Python |
|---|---|---|
| opt | zero or one execution | if ... (no else) |
| alt | choose exactly one branch | if ... elif ... else ... |
| loop | repeat zero or more times | for / while |
Fragments You May Encounter Later
- par — parallel branches execute concurrently (e.g.,
asyncio.gather) - break — exit the enclosing loop
- ref — an “interaction use”; a named sub-scenario referenced from another diagram
- critical — an atomic region
- neg — an invalid trace (what must not happen)
Arrow Cheat Sheet
->synchronous (caller blocks)-->return (dashed, open arrow)->>asynchronous (caller keeps going — you will meet this later)-> selfself-call
Guidelines You Should Remember
- Lifelines are instances, not classes. Two
Dog()calls → two lifelines. - Activation boxes are stack frames. They start on the way in, end on the way out. Nested activation = nested calls.
- Do not draw every
ifandfor. One or two fragment levels is usually enough — split deeply-branching logic into multiple diagrams. - One scenario per diagram. A sequence diagram answers a single question. Happy path, error path, and edge cases typically belong in separate diagrams.
- Only draw return arrows when the value matters. UML is about communication — if the return is
Noneor implied by the activation box ending, skip the dashed arrow. - Real diagrams do not start from
Main. In this tutorial every scenario began from__main__to give you a Python anchor for every arrow. In practice, sequence diagrams focus on a specific interaction between objects that are already running — they start at an interesting method call, not at program startup. A whiteboard diagram might open withuser -> authService: login(password)and never show howuserorauthServicewere constructed. TheMainlifeline was a learning scaffold; leave it behind in your own diagrams.
What Sequence Diagrams Are Good For
- Designing an interaction before you write the code
- Explaining a specific scenario to a teammate or reviewer (much faster than prose)
- Documenting a protocol (API handshake, auth flow, publish/subscribe)
- Finding a bug — draw the diagram of what you expect vs. what actually happens
And what they are not good for: showing the complete behavior of a system. Use a class diagram for structure and use multiple small sequence diagrams for specific runtime scenarios.
Next up: you now know both halves of UML modeling — structure (class diagrams) and behavior (sequence diagrams). In your software engineering career you will mix and match these constantly, usually on whiteboards, usually for five minutes at a time. That is the sweet spot UML was designed for.
# Sequence Diagram Reference
Nothing to code in this step — it is a summary page.
Use it as a cheat sheet when working on future sequence diagrams.