Lesson 3.1Lesson 3.1 · Functions, Modules & Files
Writing Functions
Give a useful piece of work a name, and you can reuse it anywhere without copying a single line
You have written the same window-to-floor ratio three times this project. A function lets you write it once, name it, and never type it again.
Up to now your scripts have run top to bottom - a single list of instructions. That works until you notice you are copying the same few lines around: the same area formula, the same unit conversion, the same little rule. Copy-paste feels quick, but every copy is a place a bug can hide and a change you will forget to make everywhere.
A function is the fix. It takes a useful piece of work, gives it a name, and lets you run it again with different inputs whenever you need. This lesson is the turning point where your code stops being one long script and starts becoming a small kit of named tools you build up over a career - starting with a real design calculation you will package today.
Function = named work + inputs + return. Write the rule once; call it forever.
What a function is, and the shape of one
A function is a named block of instructions you can run - call - as many times as you like. You have already used functions written by other people: print(), len(), round(). Now you write your own. The keyword def (short for define) starts one:
def greet_room(name):
print(f"Designing the {name}")
greet_room("kitchen")
greet_room("bedroom")Read the shape carefully, because every function follows it. The first line is the header: def, the name you choose, then parentheses holding the inputs. It ends in a colon, and everything indented below it is the body - the work. Defining a function does not run it; the body only executes when you call it by writing its name with parentheses, as the last two lines do.
Naming the work is the whole point. greet_room says what it does far better than three loose print lines would, and now the instruction lives in exactly one place. If you decide the message should change, you edit the body once and every call updates. That is the quiet superpower of functions: one name, one home, one place to change.
It helps to notice how a function changes the reading of a script, not just its writing. A well-named function turns a wall of detail into a summary you can scan: load_rooms(), then filter_large(), then write_schedule() reads almost like a sentence describing the plan, while the fiddly detail hides inside each function until you need it. This is the same instinct you use in a drawing set - a general arrangement plan gives the overview, and detail drawings hold the specifics. Functions let your code work at both altitudes at once: a clear high-level story on top, precise instructions underneath. Names are free; spend them generously and describe what a function does, not how - paint_litres, not calc1.
def name(inputs): then an indented body. Nothing runs until you call it.
Parameters, arguments and returning a value
Most useful functions take inputs and hand back a result. The names in the header are parameters - placeholders. The real values you pass when calling are arguments. And the return statement is how a function gives an answer back to whoever called it:
def window_ratio(glass_area, floor_area):
return glass_area / floor_area
living = window_ratio(2.4, 18.0)
print(round(living, 3)) # 0.133Here glass_area and floor_area are parameters; 2.4 and 18.0 are the arguments. When Python reaches return, it stops the function and sends that value back, so living now holds 0.133. This is different from print, which only shows something on screen - return produces a value you can store, compare or feed into another function.
A function can take no inputs, one, or many, and it can return anything: a number, a string, a list, even a dictionary of results. If you never write return, the function still runs but hands back the special value None. A good rule while learning: if the function computes something, return it; if it only reports to a human, print it. Mixing the two is a classic beginner tangle - keep calculation and display separate and your functions stay reusable.
You can also hand back more than one thing at once. Returning several values - return floor_area, wall_area, perimeter - actually returns a tuple, which the caller can unpack into separate variables in one line: f, w, p = room_metrics(4, 5, 3). That keeps related results together without inventing a separate function for each. And a function can return early: the moment a return runs, the function ends and no later lines execute, which is handy for handling a special case up front (return zero if an area is negative, say) before doing the main work. Between inputs through parameters and answers through return, you now have the full contract of a function - a clear boundary of what goes in and what comes out, which is exactly what lets you trust and reuse it without re-reading the body every time.
Parameters are placeholders; arguments are the real values. return hands the answer back.
Scope: what happens inside stays inside
When a function runs, the names it creates live in their own private space called its local scope. They exist only while the function runs and vanish when it returns. This is a feature, not a nuisance - it means a variable named total inside one function cannot secretly clash with a total somewhere else.
def tile_count(area, tile):
per_tile = area / tile # local - lives only in here
return per_tile
print(tile_count(20, 0.36))
print(per_tile) # NameError: not defined out hereThe last line fails because per_tile never existed outside the function. Names created inside are invisible outside; that isolation is exactly what lets you drop a function into any script without worrying what it will disturb. A function can read a variable from the surrounding (global) scope if it does not have its own by that name, but leaning on that makes functions fragile and hard to move. The clean habit is simple: pass everything a function needs in through its parameters, and send everything it produces back through return. Treat each function as a sealed box with labelled inputs and outputs, and it will behave the same wherever you use it - which is precisely what makes it reusable.
There is a tempting shortcut that scope should steer you away from: reaching out to grab a global variable instead of passing it in. A function that quietly reads a global TILE_SIZE works fine today and then breaks the moment you copy it into another script that has no such variable - or worse, silently uses the wrong one. The failure is invisible because nothing in the function's header hints at the hidden dependency. Passing every input explicitly makes the function honest: its header lists everything it needs, so you can read one line and know exactly what it depends on. Constants that genuinely never change are the mild exception, but as a default, if a function uses a value, let it arrive as a parameter. This discipline is what lets you lift a function out of one project and drop it straight into the next.
Locals are sealed inside. Inputs in through parameters, results out through return.
Default arguments and keyword arguments
Real design tools have sensible defaults you can override. Python lets a parameter carry a default value used when the caller does not supply one:
def paint_litres(wall_area, coats=2, coverage=10):
return wall_area * coats / coverage
print(paint_litres(45)) # uses 2 coats, 10 m2/L -> 9.0
print(paint_litres(45, coats=3)) # override just the coats -> 13.5Because coats and coverage have defaults, the everyday call is short, but the flexibility is there when a job needs it. Naming an argument in the call - coats=3 - is a keyword argument, and it is worth the few extra characters: it reads clearly and you can skip straight to the one you want without counting positions. One trap to know now: never use a mutable value like a list as a default (def f(items=[])) - that single list is shared across every call and causes baffling bugs. Use None and build the list inside instead.
Defaults are how you encode your practice's house rules while leaving room for exceptions. Two coats of paint, ten percent wastage on tiles, a 300mm setting-out grid - these are the values that are right most of the time, so they belong as defaults, letting the everyday call stay short and the unusual job override just the one parameter it needs. Order matters slightly: parameters with defaults must come after those without, because Python fills positional arguments left to right. And once you have a few defaults, prefer to override them by name in the call - paint_litres(45, coverage=8) is unmistakable, whereas paint_litres(45, 2, 8) forces the reader to remember what the third number means. Clear calls are as much a part of a good function as a clear body.
Defaults keep the common call short; keyword arguments keep the unusual call readable.
Docstrings, DRY, and packaging a design calculation
A function is also the natural home for a short note on what it does. A docstring - a string on the first line of the body - is that note, and tools (and help()) read it:
def window_ratio(glass_area, floor_area):
"""Return the window-to-floor ratio (glass area / floor area).
Both areas in the same units (m2). A common daylight rule of
thumb asks for at least 0.10 in habitable rooms.
"""
return glass_area / floor_areaThis is the payoff of the whole lesson and the principle behind it: DRY - Do not Repeat Yourself. Every rule your practice uses - a daylight ratio, a paint estimate, a stair-riser check - deserves to live in exactly one named, documented function rather than scattered as copied lines. Build a few of these and you have the beginnings of a personal toolkit: named, tested, reusable moves you trust. When a code or a standard changes, you fix the rule in one place and every script that calls it is instantly correct. That is how a pile of throwaway scripts slowly turns into leverage you keep.
Docstrings repay the small effort in a particular way for designers: they capture the assumptions that would otherwise be lost. Which units? Which rule of thumb, from which standard? What does the function do at the edges - a zero floor area, a negative input? Writing that down in the docstring, at the moment you understand it, saves the future colleague (often you, months later) from reverse-engineering the intent from the arithmetic. Aim for a one-line summary first, then a few lines of detail if the function earns it. You do not need heavy documentation for a fifteen-line script - but a single honest sentence about what a function returns and in what units is almost always worth typing. Between good names, small focused functions and a plain docstring, ordinary scripts start to read like something you could hand to another person - which, eventually, you will.
A docstring says what and why. DRY: one rule, one function, one place to fix.
def / function
Naming a reusable block of instructions
The core of the lesson: define once with def, call by name as often as you like.
return
Sending a computed value back to the caller
Produces a value you can store or reuse, unlike print which only displays. No return means the function yields None.
parameter vs argument
Placeholder in the header vs the real value passed in
Parameters name the inputs; arguments are what you actually hand over when you call.
local scope
Names created inside a function stay inside
Isolation that makes a function safe to drop into any script - pass inputs in, return results out.
docstring
A short description as the first line of the body
Documents what a function does and why; help() and editors surface it. Part of writing code others (and future you) can use.
Workshop - turn a design rule into a reusable function
You will take one calculation you do by hand and package it as a clean, documented function with a sensible default - the first tool in your personal kit.
Python 3 (any editor or notebook). No libraries required.
Goal: write and call a reusable design function Inputs: one calculation you know (paint, tiles, a ratio) Time: ~30 minutes
- 1Pick one rule you compute often. Write it once, inline, with real numbers, and check the answer is right.
- 2Wrap it in a function with a clear name and named parameters - for example def paintlitres(wallarea, coats, coverage): - and return the result instead of printing it.
- 3Give one parameter a sensible default (coats=2) so the common call is short, then call the function twice: once using the default, once overriding it with a keyword argument.
- 4Add a docstring on the first line saying what it returns, the expected units, and any rule-of-thumb it assumes. Run help(your_function) to see it.
- 5Prove scope: try to print one of the function's inside variables from outside and watch it fail - then explain in a comment why that failure is a good thing.
You’ll walk away with
A single .py file with one documented function that takes parameters, uses a default, returns a value, and is called at least twice with different inputs - plus a one-line note on why you returned rather than printed.
Three altitudes on the same idea
Read the band that fits you — or all three.
Your practice runs on repeated rules - encode them as functions. Daylight and window-to-floor ratios, occupancy loads, stair and ramp checks, area take-offs: each is a rule you apply again and again across projects. Wrapped as documented functions, they become a trusted in-house library that any script - or any colleague - can call, and when a code updates you correct it in exactly one place.
The estimates you redo on every project are functions waiting to happen. Paint litres, tile and flooring quantities with a wastage allowance, curtain fabric from window sizes, lighting counts per area - package each once with sensible defaults (two coats, ten percent wastage) and you turn a fiddly evening of arithmetic into a one-line call you can trust and reuse job after job.
Functions are where scripts start to feel like real programming - and studios notice. They are also how you keep an assignment readable: a clear function with a docstring beats a wall of repeated lines every time. Master parameters, return values and scope now; every later part of this course, from pandas to Grasshopper, is built out of functions you write and call.
“A function is only worth writing if you are going to use it many times.”
Do it yourself
Reason about each before you run it.
- 1In your own words, what is the difference between defining a function and calling it?
- 2What does return do that print does not?
- 3Which are parameters and which are arguments in def area(l, w): ... called as area(3, 4)?
- 4Why can't you read a function's local variable from outside it - and why is that useful?
- 5Rewrite paint_litres so coverage defaults to 10 and show one call that overrides it.
The one line to carry out
Peer-reviewed journals & authoritative standards
- 01Subroutine — Wikipedia, 2026.
- 02The Python Tutorial — Python Software Foundation, 2026.
- 03Software documentation — Wikipedia, 2026.
- 04Modular programming — Wikipedia, 2026.
Your own functions are one source of reusable code; the other is the enormous body of code other people have already written. Next we learn to import it - the standard library, pip and packages - so you rarely start from scratch.
The author
Amogh N P
Architect, interior designer, and creative polymath. Studio Matrx began in his notebooks — his vision of design made honest, useful, and open to everyone. Its Academy is written and taught in his memory, and free, forever.
More about Amogh →