The Python She Runs On
She is a pipeline, so return beats print
GLaDOS is going to be a pipeline: a microphone feeds a speech recognizer, which feeds a language model, which feeds a voice synthesizer. For that chain to work, every piece has to hand its result to the next piece. So the first Python skill this book insists on is writing functions whose output can be captured, and the enemy of that skill is the most natural habit in the language.
The tempting shortcut is to make functions print() directly. It feels
productive; you see output immediately. But a function that prints is a dead end. You
cannot feed its text to a speech engine, write it to a log, or assert on it in a test,
because the output already escaped to the terminal. The day you want her to
speak a greeting instead of printing it, every printing function has to be
rewritten. The day you want returned text spoken instead, chapter 3's
speak() takes it as an argument and nothing else changes.
The rule, stated once and used for ninety-eight more chapters: a function
computes a value and returns it; printing, speaking and logging are the caller's
job. This chapter builds a small file that practices the rule, plus the two
companions it travels with: typed signatures, and the __main__ guard you
met in chapter 1.
This chapter and the last are the only pure-Python ground school in the book; chapter 3
starts making sound. If -> str, dataclasses and the guard are old
friends, skim the failure box (the phantom-greeting bug is worth thirty seconds even
for veterans) and move on. If Python is new to you, this book teaches every construct
it uses as it uses them, and Python Zero exists when you want the language in full.
A greeting that returns
# labs/hello_glados.py
def greet(subject_name: str) -> str:
return f"Hello, {subject_name}. The facility is now online."
print(greet("Test Subject"))
$ uv run python labs/hello_glados.py
Hello, Test Subject. The facility is now online.
Look at where the printing happens: the last line, outside the function.
greet builds the text and hands it back; the print is a
caller deciding what to do with it. The type hints (: str in,
-> str out) document the contract and let your editor catch a wrong
argument before the file ever runs.
# labs/hello_glados.py
name = "GLaDOS"
status = "ACTIVE"
subject_count = 1
def greet(subject_name: str) -> str:
return f"Hello, {subject_name}. The facility is now online."
def report_status() -> None:
print(f"System: {name}")
print(f"Status: {status}")
print(f"Subjects logged: {subject_count}")
print(greet("Test Subject"))
report_status()
$ uv run python labs/hello_glados.py
Hello, Test Subject. The facility is now online.
System: GLaDOS
Status: ACTIVE
Subjects logged: 1
report_status prints on purpose, and its return type says so:
-> None announces there is nothing to hand back, because printing the
dashboard is the function's whole job. That contrast is the real lesson of this stage.
Both kinds of function are valid; you just have to know which one you are writing, and
the return type is where you say it.
# labs/hello_glados.py — full file
name = "GLaDOS"
status = "ACTIVE"
subject_count = 1
def greet(subject_name: str) -> str:
return f"Hello, {subject_name}. The facility is now online."
def report_status() -> None:
print(f"System: {name}")
print(f"Status: {status}")
print(f"Subjects logged: {subject_count}")
def build_message(speaker: str, text: str) -> str:
return f"[{speaker}]: {text}"
def main() -> None:
print(greet("Test Subject"))
report_status()
msg = build_message(name, "Interesting. You're still alive.")
print(msg)
if __name__ == "__main__":
main()
$ uv run python labs/hello_glados.py
Hello, Test Subject. The facility is now online.
System: GLaDOS
Status: ACTIVE
Subjects logged: 1
[GLaDOS]: Interesting. You're still alive.
build_message is the most reusable function in the file: two arguments in,
one formatted string out, no state touched. A pattern you will write dozens of times.
And the demo calls now live inside main() behind the guard, so the file
has become two things at once — a runnable script, and a toolbox other files can
import without triggering the demo.
Why this works: the two lives of a module
Every Python file is a module, and Python sets a variable called __name__
in each one. Its value depends entirely on how the file entered the program. Launch it
directly with python labs/hello_glados.py and __name__ is the
string "__main__"; pull it in with import hello_glados and
__name__ is "hello_glados", the module's own name.
So if __name__ == "__main__": is the file asking: am I the program being
run, or a library being borrowed? Code under the guard runs only in the first case.
One line, and the same file serves both futures. That matters here because chapter
10's voice loop imports functions from five earlier chapters, and none of their demos
should fire when it does.
Skip the guard and nothing looks wrong at first; the file runs fine on its own. The bug
appears later, when another file tries to reuse a function. Suppose
glados_voice.py was written the stage-1 way, with a call at top level:
# glados_voice.py
def greet(subject_name: str) -> str:
return f"Hello, {subject_name}. The facility is now online."
print(greet("Test Subject")) # top level — runs on import
# use_voice.py
import glados_voice
print(glados_voice.greet("Chell"))
$ uv run python labs/use_voice.py
Hello, Test Subject. The facility is now online.
Hello, Chell. The facility is now online.
The first line is a phantom greeting nobody asked for. Importing a module executes it
top to bottom, so the stray print fired during the import itself. In the
finished build that kind of stray output corrupts logs and runs setup you never
requested, at import time, before your program has even started. The fix is stage 3's
move: demo calls into main(), gated by the guard. Then the import defines
greet silently and only a direct launch produces output.
Configuration wants a dataclass
One more habit before the sound starts. Her settings (which microphone, which model,
which voice) will travel together through the whole pipeline, and the worst container
for them is a handful of loose variables. Python's dataclass turns a group
of named, typed fields into a single object with almost no ceremony:
# labs/config_demo.py
from dataclasses import dataclass
@dataclass
class VoiceConfig:
sample_rate: int = 22050
voice_model: str = "glados-v1"
volume: float = 0.8
def describe(cfg: VoiceConfig) -> str:
return f"{cfg.voice_model} at {cfg.sample_rate} Hz, volume {cfg.volume:.0%}"
if __name__ == "__main__":
cfg = VoiceConfig()
print(describe(cfg))
loud = VoiceConfig(volume=1.0)
print(describe(loud))
$ uv run python labs/config_demo.py
glados-v1 at 22050 Hz, volume 80%
glados-v1 at 22050 Hz, volume 100%
Every field has a name, a type and a default; overriding one at construction leaves
the rest alone. When chapter 3 needs a sample rate and chapter 7 needs a voice model,
they will read them from an object like this instead of from scattered globals, and
when volume 2 moves settings into files on disk, the dataclass is what those files
load into. The 22050 is not arbitrary either; you will find out what a
sample rate is, physically, two chapters from now.
Checkpoint, and the last silent chapter
- Given any function in this chapter, I can say whether it returns a value or exists for a side effect, and point to the return type that declares which.
- I can predict what
x = report_status()leaves inx, and why the three lines still print. - I can explain the phantom greeting: what runs at import time, and which single line prevents it.
- I can build a
VoiceConfigwith one field overridden and the rest defaulted.
Exercise 1 — her first insult. Add a function
taunt(subject_name: str, test_count: int) -> str that returns a line
like "Congratulations, Chell. You have failed 47 tests. That's a new
record." Wire it into main(). Where does the printing go?
The function returns the f-string and prints nothing;
main() calls print(taunt("Chell", 47)). If your
taunt contains a print, you have rebuilt the dead end
this chapter exists to remove: chapter 10 needs that string for the speech
engine, not the terminal.
Exercise 2 — prove the guard again, from the other side.
Create use_hello.py containing only
import hello_glados and run it. What prints, and what would print if you
deleted the guard from hello_glados.py?
With the guard: nothing. The import defines four functions and stops. Without it,
main() would fire mid-import and the whole demo (greeting, status
dashboard, message) would spill out before use_hello.py ran its own
first line.
Exercise 3 — extend the config. Add a
device_index: int | None = None field to VoiceConfig,
where None means "system default microphone." Why is
None the right default rather than 0?
Because 0 is a real device index, the first one in the list, and on
many machines it is not the default microphone. None says "no
preference, let the audio library choose," which is a different statement from
"use device zero." Chapter 4 hits exactly this distinction when it enumerates your
machine's devices.
That closes the ground school. You have a reproducible workspace, and the three habits the pipeline is built from: return the value, type the signature, guard the entry point. Next chapter, the machine speaks its first words — through someone else's voice for now, because hers takes a dataset you have not collected yet.