Python Functions and Logic Building Explained
Write reusable Python functions — def, parameters, return values, default and keyword arguments, and scope — and break problems into pieces.

Write a useful block of code once, give it a name, and you can run it anywhere just by saying that name. That's a function. Instead of copy-pasting the same five lines into ten places, then fixing the same bug in all ten when it breaks, you write it once, name it, and call it. This lesson is where your programs stop being one long script and start being a set of named, reusable pieces.
Defining a function
Here's the smallest useful one. Run it.
Three things are happening. def greet(): says "I'm defining a function called greet." Everything indented under it is the function body, the code that runs when you use it. And greet(), with the parentheses, is how you actually call it: run the body now.
Notice the function ran twice because we called it twice. Defining a function doesn't run anything; it just teaches Python what greet means. Nothing happens until you call it. That split is the whole point. You write the recipe once, then cook from it as many times as you want.
The indentation isn't decoration. Python uses it to know where the body starts and stops: every indented line belongs to greet, and the first un-indented line is back outside the function. Same rule you saw with loops and if.
Parameters and return values
A function that always says the same thing gets old fast. Real functions take input. You hand values in through parameters:
name is a parameter, a placeholder named in the def line. When you call greet('Ada'), the string 'Ada' (the argument) gets slotted into name for that run. Call it with 'Grace' and name is 'Grace' instead. One function, different output each time, depending on what you pass.
Now the piece that trips up almost everyone at first: printing and returning are not the same thing. print shows something on screen for a human to read. return hands a value back to the code that called the function, so the program can keep using it.
area(3, 4) computes 3 * 4 and hands back 12. That value lands in result, and it's a real number you can do math on — see how area(3, 4) * 2 gives 24. If area had used print instead of return, you'd see 12 on screen but result would be empty, and multiplying it would blow up. Print is for people. Return is for your program.
Quick check
What does a function return if it has no return statement?
So a function without a return still hands something back: None. That's Python's word for "nothing here." It's why result = some_function_that_only_prints() leaves result holding None, not the text you saw printed.
Default and keyword arguments
Sometimes a parameter has a sensible fallback and you don't want to type it every single time. Give it a default value right in the def line:
greeting='Hi' means "if the caller doesn't supply a greeting, use 'Hi'." So greet('Ada') prints Hi, Ada, and greet('Ada', 'Welcome back') overrides the default. Defaults make the common case short and still let you customize when you need to.
You can also name arguments as you pass them, which is called using keyword arguments:
greet(name='Ada')
greet(name='Ada', greeting='Howdy')Passing name='Ada' instead of just 'Ada' does the same thing, but spells out which parameter you mean. That's a real win once a function takes four or five arguments and you can't remember the order — make_user(name='Ada', admin=True) reads clearly, make_user('Ada', True) makes you go check the definition. Two rules to know: keyword arguments come after any plain ones in a call, and parameters with defaults come after the ones without in the def.
Scope
Try to print secret from outside the function below and Python complains it doesn't exist:
def stash():
secret = 42
print('inside:', secret)
stash()
print(secret) # NameError: name 'secret' is not definedA variable created inside a function lives and dies inside that function. secret exists while stash runs and is gone the moment it returns. The function got its own private workspace, called its local scope, and the outside world can't see in. This is a feature, not a nuisance: it means a variable named i or total inside one function can't accidentally clobber an i or total somewhere else. Functions don't step on each other.
Variables defined at the top level of your file, outside any function, are global, and functions can read them. Writing to a global from inside a function needs an explicit global keyword, and you should reach for that almost never. Passing values in as parameters and handing results back with return keeps each function self-contained and far easier to reason about.
Breaking problems into functions
This is the real reason functions exist, and it's worth more than all the syntax above. A big problem is hard to hold in your head all at once. Split it into a few small jobs, give each one a name, and suddenly you're solving four easy things instead of one hard thing.
The FLAMES project from the last lesson is a clean example. We wrapped the whole thing in one flames() function, but look at what it actually does and it's four distinct steps, each begging to be its own function:
def clean(name):
# strip a name down to plain lowercase letters
return [c for c in name.lower() if c.isalpha()]
def cancel(a, b):
# remove the letters the two names share
for letter in a[:]:
if letter in b:
a.remove(letter)
b.remove(letter)
return a, b
def count(a, b):
# how many letters are left over
return len(a) + len(b)
def result(n):
# count n out around FLAMES, return the verdict
words = ['Friends', 'Love', 'Affection', 'Marriage', 'Enemy', 'Siblings']
index = 0
while len(words) > 1:
index = (index + n - 1) % len(words)
words.pop(index)
index = index % len(words)
return words[0]Now the top-level logic reads like a sentence: clean both names, cancel the shared letters, count what's left, turn that count into a result. Each function does one job, has a name that says what that job is, and you can test it on its own. Is cancelling broken? Call cancel with two lists and check the output — you don't have to run the whole game to find the bug. That's the payoff: small named pieces you can build, test, and fix independently, then snap together.
One function, one job
If a function is hard to name, that's usually a sign it's doing too much. clean and count name themselves because each does exactly one thing. When you catch yourself wanting to call something clean_and_count_and_format, that's three functions wearing a trenchcoat — split it.
Recap and what's next
A function is a named, reusable block of code: def defines it, the indented body is what runs, and calling it by name with () runs that body. Parameters let you pass values in, return hands a value back out (and no return means you get None), defaults and keyword arguments make calls shorter and clearer, and scope keeps each function's variables to itself. Most of all, functions let you break a big problem into small named jobs you can reason about one at a time.
You've now got the full toolkit for writing your own Python from scratch. Next we go past the code you write yourself and pull in code other people wrote: modules, pip and virtual environments, where one import line gives you thousands of ready-made functions.

Written by
Rhythm Bhiwani
Engineer and relentless builder, happiest reverse-engineering hard problems until they click.
Enjoyed this?
Tap the heart to leave some love.
Be the first to react
Comments
Join the conversation.
Loading comments…


