All 24 modules are open from the start — nothing here is
locked, and nothing costs anything. Sign in so your progress, titles and
credentials stay with you, on every device you use.
Free forever, with your Google account. No password, no payment.
Functions worked in the last module. This one is about the two places
they surprise people: the rules for what you can pass and in what order, and the
fact that a name inside a function is not the same name as the one outside it —
except when it is, and except when the thing it points at can be changed from
either side.
Ready?
1
Positional, Keyword, *args, **kwargs
Two ways to pass a value in, and two ways to accept however many turn up.
def send(message, retry=False):
...
send("hello") # positional
send("hello", True) # positional
send("hello", retry=True) # keyword — says what the True means
Keyword arguments are worth reaching for whenever the value alone would
not explain itself. Six months later send("hello", True)
needs the definition open to read; retry=True does not.
When the number of arguments is not fixed, a star collects the extras:
def totals(*numbers): # numbers is a tuple
return len(numbers), sum(numbers)
def render(**options): # options is a dict
...
totals(1, 2, 3) # (3, 6)
totals() # (0, 0) — no arguments is fine
render(port=8080, debug=True)
The order in a definition is fixed and Python enforces it: ordinary
parameters, then ones with defaults, then *args, then
**kwargs. Put a plain parameter after a defaulted one and
the def line itself is a SyntaxError, because
there would be no way to tell which value you meant to leave out.
Quick check
Why is def f(a=1, b): a SyntaxError?
2
The Default That Is Created Once
This is the most famous trap in the language, and it catches everyone
exactly once:
The default is evaluated once, when the function is
defined — not once per call. So there is exactly one list, it
belongs to the function, and every call that does not supply its own
keeps adding to it.
With an immutable default — 0, "",
None, a tuple — this is invisible, because nothing can
change the value in place. With a list, a dictionary or a set, it is a
bug that grows with use.
The fix is always the same shape:
def add_tag(tag, tags=None):
if tags is None:
tags = []
tags.append(tag)
return tags
None is the "nothing was supplied" marker, and the fresh list
is made inside the body, which does run once per call.
Why is None rather than if not tags
if not tags: is also true for an empty list somebody
deliberately passed in, so it quietly replaces their list with a
different one. is None asks the question that was actually
being asked: was anything supplied at all?
Quick check
When is a mutable default harmless?
3
Which Name Did You Mean?
Names created inside a function are local to it. They
appear when it is called and disappear when it returns, and nothing
outside can see them.
Reading a name the function did not create looks outward, to the module:
RATE = 0.2
def with_tax(amount):
return amount * (1 + RATE) # reading RATE is fine
Assigning is different. If a function assigns to a name anywhere
in its body, that name is local for the whole function — decided
when the function is compiled, before a single line runs. Which produces
this:
The assignment on that line made count local, so the
right-hand side is reading a local that has not been given a value yet.
The message —
cannot access local variable 'count' where it is not associated
with a value — is exact, and only makes sense once you know the
rule.
global count at the top of the function makes it work, and
you should almost always do something else instead: take the value in as
an argument and hand the new one back. A function that reaches out and
changes module state is one you cannot test without setting the world up
first.
Needs the world set up
def bump(): global count count += 1
Takes nothing, returns nothing, and only works in one program.
Testable in one line
def bump(count): return count + 1
Value in, value out. The caller decides what to do with it.
Quick check
A function reads RATE and never assigns to it. Does it need global?
4
A Function Can Change What You Handed It
Rebinding a parameter inside a function affects nothing outside — the
parameter is a local name, and pointing it somewhere else is a local
event.
But if the thing passed in is mutable, the function and the
caller are looking at one object, and a change through either is visible
to both. This is the aliasing from module 2-04, arriving through the
front door:
def add_default_tag(tags):
tags.append("new") # changes the caller's list
return tags
def add_default_tag_safely(tags):
return tags + ["new"] # builds a new one
Neither is wrong. What is wrong is doing the first while the name suggests
the second. If a function changes what it was given, the name should say
so — sort_in_place, append_default — and it
should not also hand back a value, because a function that both mutates
and returns invites a caller to use it both ways.
Rebinding is local
tags = [] inside a function points the local name elsewhere. The caller's list is untouched.
Mutating is not
tags.append(x) changes the one object both names point at.
Copy at the boundary
tags = tags.copy() as the first line, when the caller should be protected.
Or say so in the name
Mutation is fine when it is the advertised job.
The test that settles it
Could you call this function twice with the same input and get the same
answer both times? If not, something is being carried between calls —
a mutable default, a global, or the caller's own data being edited
underneath them.
Arguments in, value out, nothing else touched. Everything in this module
is a way that quietly stops being true.
Quick check
Inside a function, tags = [] where tags is a parameter. What does the caller see?
0 of 9 completed
Real Python runs right here in your browser — nothing to install, nothing
sent to a server. The interpreter downloads once the first time you press
Run, then stays cached.
01
To do
An argument can go in by position or by name. Naming it costs a few
characters and buys a line that explains itself:
Keyword arguments can also come in any order, since the name says where
each one belongs.
Your task: write
describe(name, level, active) returning a single line, then
print three calls — all positional, all by keyword in a different order,
and a mix:
Kenji is level 3 (active) Ada is level 7 (active) Grace is level 1 (inactive)
The word in brackets is active when the flag is true and
inactive when it is not.
your_code.py
PythonCtrl↵ to run
Hint
A conditional expression handles the bracket: "active" if active else "inactive". Then return an f-string built from the three values.
Output
02
To do
A parameter with a default may not be followed by one without. Python
rejects the def line itself, before anything runs.
The reason is that a call like f(5) would be ambiguous —
there would be no way to tell which parameter the 5 was for.
Your task: run it, read the SyntaxError,
then reorder the parameters so the three calls below work unchanged:
3750 3375.0 1875.0
your_code.py
PythonCtrl↵ to run
Hint
quantity and price are required, so they go first. discount has a default, so it goes last. The calls all name their arguments, so nothing else needs changing.
Output
03
To do
The most famous trap in the language. A default is evaluated
once, when the function is defined — not once per call.
So there is exactly one list, it belongs to the function, and every call
that does not supply its own keeps adding to it.
The fix is always the same shape: default to None, and build
the fresh list inside the body, which does run once per call.
Your task: run it and watch the list grow, then fix it so
the output is:
['a'] ['b'] ['x', 'c']
The third call supplies its own list, and must still work.
your_code.py
PythonCtrl↵ to run
Hint
Default tags to None, then `if tags is None: tags = []` as the first line of the body. Use `is None` rather than `if not tags`, or an empty list somebody passed in gets replaced too.
Output
04
To do
A starred parameter collects every extra positional argument into a
tuple. Zero of them is fine — the tuple is simply empty.
Your task: write describe(name, **options)
returning the name followed by each option in alphabetical order, and
print two calls:
server: debug=True, port=8080 server: (no options)
With no options at all, the second half reads
(no options).
your_code.py
PythonCtrl↵ to run
Hint
`if not options: return f"{name}: (no options)"` handles the empty case first. Otherwise build a list of f"{k}={v}" from sorted(options.items()) and join it with ", ".
Output
06
To do
A name created inside a function is local. It appears
when the function is called and disappears when it returns, and nothing
outside can see it — even when the name is the same.
Reading a module-level name from inside a function is fine and needs
nothing special. It is only assignment that makes a name local.
Your task: run it and predict the output before you read
it. Then leave the function alone and print the three values, in this
order — what the function returned, the outer total, and
RATE:
120.0 100 0.2
your_code.py
PythonCtrl↵ to run
Hint
The total inside the function is a different name from the one outside — assigning to it created a local. The outer one is untouched.
Output
07
To do
If a function assigns to a name anywhere in its body, that name
is local for the whole function — decided when the function is compiled,
before a single line runs.
So count = count + 1 makes count local, and then
the right-hand side reads a local that has not been given a value yet.
Hence
UnboundLocalError: cannot access local variable 'count' where it is
not associated with a value.
global would silence it. The better answer is almost always
to take the value in and hand the new one back — a function that reaches
out and edits module state cannot be tested without setting the world up
first.
Your task: run it, read the error, then rewrite
bump to take the current count and return the new one. Keep
the module-level count and update it at the call:
1 2
your_code.py
PythonCtrl↵ to run
Hint
def bump(count): return count + 1. Then at each call, count = bump(count) before printing it — the caller owns the value, the function just does the arithmetic.
Output
08
To do
Handing a list to a function does not copy it. The function's parameter
and the caller's name point at the same object, so
tags.append(...) inside is visible outside — the aliasing
from module 2-04, arriving through the front door.
This is not always wrong. It is wrong when the name does not say so, and
it is wrong here: with_default_tag sounds like it hands
something back, not like it edits what it was given.
Your task: make the function leave its argument alone,
building and returning a new list instead:
['a', 'b', 'new'] ['a', 'b']
your_code.py
PythonCtrl↵ to run
Hint
`return tags + ["new"]` builds a new list and leaves the original untouched. Adding two lists never modifies either one.
Output
09
To do
Sometimes changing the thing you were given is exactly right — sorting a
list in place, filling a cache, adding to a queue. Two rules make it
safe:
1
Say so in the name
append_default, sort_in_place. Not with_... or get_....
2
Do not also return it
Returning the same object invites a caller to treat it as a copy. Python's own list.sort() returns None for exactly this reason.
And when a function must not touch what it was given, copy at the
boundary — first line of the body, before anything else can go wrong.
Your task: write both.
append_default(tags) adds "new" to the caller's
list and returns nothing. safe_copy(tags) takes a copy first
and returns a changed one, leaving the original alone:
None ['a', 'b', 'new'] ['x', 'new'] ['x']
your_code.py
PythonCtrl↵ to run
Hint
append_default is one line: tags.append("new"), with no return at all — the None it hands back is the honest answer. safe_copy starts with tags = tags.copy(), then appends and returns.
Output
The settings resolver
To do
Every service in the dojo starts by working out its settings: a set of
defaults, whatever the deployment overrides, and whatever was passed on the
command line. Write the function that resolves them — and does it without
damaging anything it was handed.
Write these two, with exactly these names:
resolve(overrides=None, **extras) — returns the settings.
Start from DEFAULTS, apply overrides if one was
given, then apply extras on top, so a command-line value
beats a file value and a file value beats a default.
describe(settings) — returns one line per setting, in
alphabetical order, as key=value joined by ", ".
Returns "(none)" for empty settings.
Neither may damage anything. After any number of calls,
DEFAULTS and the caller's own dictionary must be exactly as they
started. Both functions need a docstring of at least twenty characters, and
neither may print.
The first is the defaults alone, the second adds the file overrides, the
third adds a command-line debug, and the fourth is
DEFAULTS, proving it survived.
your_code.py
PythonCtrl↵ to run
Hint
Start with settings = DEFAULTS.copy(). If overrides is not None, settings.update(overrides). Then settings.update(extras) — extras is always a dict, empty when nothing was passed, so it needs no guard. Default overrides to None rather than {}, or every call shares one dictionary. describe guards the empty case, then builds f"{k}={v}" from sorted(settings.items()) and joins with ", ".