What you'll learn
So far, we've used variables, conditions, loops and structured data to make reports. Functions let us give those operations names, test them independently, and reuse them without repeating implementation details.
- Define and call functions without confusing definition with execution.
- Pass values using positional and keyword arguments and describe parameters.
- Explain return versus print and recognize implicit None.
- Keep variables local and understand when mutable inputs can change.
- Use defaults safely and avoid shared mutable default arguments.
- Compose, document and test functions using realistic IT datasets.
- Compare generator expressions with explicit counting loops before using them in a summary.
Define it once, call it when needed
A function gives a group of instructions a name. The def keyword creates the function, but its indented body does not execute until you call that name. This difference matters in IT automation: defining a device-health check does not contact or inspect anything by itself. Functions make repeated operations understandable and prevent copy-and-paste logic from drifting into inconsistent versions.
Definition versus execution
def label(host): creates a function object. label('SW-01') calls it. If you never call a function, its body does not run.
Indentation is part of the interface
Statements indented under def belong to the function. Statements at the left margin continue running in the surrounding module.
def show_device(host):
print(f'Checking {host}')
print('Start')
show_device('SW-01')
show_device('AP-02')What prints first, and how many times does the function body execute? Reveal explanation after you predict.
Start prints first. Then Checking SW-01 and Checking AP-02 print, because show_device is called twice. Defining it alone prints nothing.
Parameters and arguments are different
A parameter is the name written in a function definition, while an argument is the actual value provided at a call site. Each call binds its argument to a new local parameter name. This is how one function works for many hostnames or measurements. Passing the correct number of arguments matters: Python raises TypeError for missing required arguments or extra positional arguments.
Parameter is the placeholder
def classify_latency(milliseconds): introduces milliseconds as a local parameter; the name can take a different value on every call.
Argument is the supplied value
classify_latency(220) passes integer 220. The caller controls the input, while the function specifies what it needs to operate.
def classify_latency(milliseconds):
if milliseconds >= 200:
return 'critical'
return 'normal'
print(classify_latency(35))
print(classify_latency(240))Does changing the argument change the function definition? Reveal explanation after you predict.
No. The same definition executes twice with two different values bound to milliseconds, printing normal followed by critical.
Follow the value into—and out of—a function.
Select a function, adjust the input, then step through the call. Switch between return and print to see why they are not interchangeable.
def classify_latency(milliseconds):
result = 'critical' if milliseconds >= 200 else 'normal'
return result
level = classify_latency(240)The caller evaluates the argument and invokes the named function.
classify_latency(240)This visualizer models function behavior; use the real Python workbench below to execute and inspect actual code. All sample values are fictional.
Return a value instead of only printing
print displays text but does not make a computed result available to the caller. return sends an object back to the expression that called the function and exits that call. A function that reaches the end without an explicit return returns None. If you assign the result of a print-only helper, the variable will contain None—not the displayed message. Returning values makes functions composable and testable.
Printing is a side effect
A printed line is useful for visibility, but calling code cannot calculate with a console message unless it also receives the value.
Return stops the current call
return exits the function immediately. A later statement in that call does not execute; the caller can store or pass the returned value.
def sum_ports(a, b):
return a + b
count = sum_ports(2, 3)
print(count + 1)What does count contain? What changes if return becomes print? Reveal explanation after you predict.
count is the integer 5, and the output is 6. Replacing return with print would print 5 inside the helper but assign None to count, causing TypeError at count + 1.
Local scope and clean boundaries
Variables assigned inside a function normally belong to that function's local scope. They do not become available at module level just because the function ran. Using return makes the boundary explicit. An immutable argument such as a string cannot be altered in place, while mutating a list or dictionary passed into a function can affect the caller; choose whether to return a new value or intentionally modify an object.
Local names stay local
Inside normalize(host), the name cleaned refers to the lowercase version; trying to print cleaned outside raises NameError unless it is returned and assigned.
Avoid accidental shared mutation
A function that appends to an input list changes the caller's list. Build a new list for filtering unless mutation is an intentional documented behavior.
def normalize(host):
cleaned = host.strip().upper()
return cleaned
raw = ' sw-01 '
result = normalize(raw)
print(result)
print(raw)Why does the last line still contain spaces and lowercase letters? Reveal explanation after you predict.
normalize creates and returns a new string. The original raw value is unchanged; cleaned is a local name that is not available outside the function.
Defaults and keyword arguments
A default parameter provides a value when callers omit that argument. Keyword arguments make the purpose of supplied values clear and can be used in a different order from the definition. Put required parameters before parameters with defaults. Default values are evaluated when the def statement runs, so avoid a shared mutable default list or dictionary: use None and create a fresh collection inside the function when needed.
Optional does not mean unknown behavior
def ticket(host, state='unknown'): makes the omission explicit. It does not silently label unreported devices healthy.
Avoid mutable defaults
Prefer def collect(value, items=None): then if items is None: items = []. Using items=[] directly can share the same list across calls.
def ticket_label(host, state='unknown'):
return f'{host}: {state}'
print(ticket_label('SW-01'))
print(ticket_label(state='offline', host='AP-02'))Which value is used for state on the first call, and why does the second call work? Reveal explanation after you predict.
The first call uses the default string unknown. The second uses named arguments, which bind to their parameter names regardless of order.
Compose helpers into a workflow
One function can call another. This is where Python becomes much easier to maintain: a status-normalization helper can be used by a reporting function, so the same rules apply in every place. Each function should have one clear responsibility. Keep input parsing, business logic and printing separate whenever practical; this makes testing individual behaviors much simpler.
Reuse one rule
normalize_status handles extra whitespace and capitalization. A report should call it instead of copying .strip().lower() expressions in several different loops.
Create useful return contracts
A helper can return a list of offline hostnames; the caller can sort, count, format or display that list independently.
def normalize_status(value):
return value.strip().lower()
def needs_attention(device):
return normalize_status(device['status']) == 'offline'
print(needs_attention({'status': ' OFFLINE '}))Which function runs first after needs_attention is called? Reveal explanation after you predict.
needs_attention starts executing; while evaluating its return expression, it calls normalize_status, receives 'offline', and compares it to 'offline'. The final printed value is True.
Docstrings, edge cases and input checks
Functions are interfaces used by other code. A short docstring can explain what an input represents, what is returned and what errors may occur. Then test normal inputs and boundary cases: an empty list, zero, a missing field or an unexpected type. A function that works for one example but fails a second sample is not yet reliable. Use descriptive exception messages instead of silently replacing invalid measurements with made-up values.
Document behavior close to the code
The first string in a function can be its docstring. Describe arguments and returns, not a play-by-play of obvious syntax.
Test realistic boundaries
If the requirement includes '200 or more', test 199, 200 and 201. If a function counts devices, test an empty inventory as well as mixed statuses.
def is_critical(latency_ms):
"""Return True for latency at or above 200 ms."""
return latency_ms >= 200
print(is_critical(199))
print(is_critical(200))What changes between the two calls, and why is this a useful test? Reveal explanation after you predict.
The results are False then True. Testing the exact boundary reveals whether the function implements >= 200 rather than > 200.
Read generator expressions before using them
A generator expression looks like (value for item in items if condition). Unlike a list comprehension, it produces values as another operation asks for them; it does not immediately build the whole list. You may see sum(1 for device in devices if is_offline(device)), but it is simply counting one for each matching device. Begin with an explicit count and loop to understand the intent before choosing the shorter form. For beginners, clarity is more important than cleverness.
First write the understandable loop
Set count = 0, visit every device, and increase the count only when its status matches. This makes every decision visible in the trace.
Then read the compact version
sum(1 for status in statuses if status == 'offline') adds one for each match. Here sum consumes the generated ones, so you don't need an intermediate list.
statuses = ['online', 'offline', 'offline']
count = 0
for status in statuses:
if status == 'offline':
count += 1
short_count = sum(1 for status in statuses
if status == 'offline')
print(count, short_count)Why is 1 inside the generator expression? Does the generator create a list of hostnames? Reveal explanation after you predict.
Each matching status contributes the integer 1 to sum, which totals 2. It does not create a list of hostnames; a generator yields each value on demand. The expanded for loop is equally correct and often easier to debug.
Build an inventory report from two helpers
A small health-report workflow can normalize status text, filter a list of dictionaries and return a summary. It is easier to reuse than one large script because data cleaning can change without rewriting the whole report. Use fictional inventory records here; the browser lesson does not connect to customer devices. The next milestone will introduce file and structured-data inputs so the same helpers can process stored reports.
Keep transformations pure
Read each record and build a new result; do not remove entries from the original inventory while looping.
Report without hiding unknowns
Count offline devices separately from unknown statuses. An absent status is not proof of online health.
def is_offline(device):
return device.get('status', 'unknown').strip().lower() == 'offline'
def count_offline(devices):
return sum(1 for device in devices if is_offline(device))
records = [{'status': ' OFFLINE '}, {'status': 'online'}, {}]
print(count_offline(records))Why is the count one rather than two? Reveal explanation after you predict.
Only the first record normalizes to offline. The last record has no status, so get supplies unknown, which is not treated as offline.
Give your first reusable helper a return value
Run actual Python in your browser. The first exercise repairs a print-only hostname formatter and verifies it with two inputs. Next, move into arguments, defaults, scope and a two-function inventory report.
Write it. Run it. Understand it.
Define reusable helpers, pass different arguments, and return results your caller can use. Verify each case in real Python.
Return a reusable hostname label
The fictional Northstar inventory needs consistent uppercase hostname labels. Fix label_host() so it returns an uppercase trimmed hostname rather than printing it. Store both call results in first and second; print them on separate lines as 'SW-01' and 'AP-02'. The function must work on two different inputs.
SW-01
AP-02Try solving it before asking for help. Hints never show a completed program.
Your printed output and errors will appear here.
Run the code, then step through the executed lines to watch variable values change.
Write it once. Test it with different inputs.
Six original coding exercises check both console output and important returned variables. Each challenge includes optional progressive hints rather than an immediate completed answer.
Continue to the Functions coding clinic →Reference material
Refer to the official Python documentation for function definitions, arguments and return behavior. You can return to these references as your scripts get more complex.
