§6  Functions
Python Programming Series  ·  Article 6

Functions in Python: A Complete Guide

A function is a named, reusable block of code: write the logic once, then run it as many times as you need with function_name(). Without functions, any piece of logic you need more than once has to be copy-pasted everywhere it's used, and every one of those copies needs to be found and fixed if the logic ever changes. Functions are the fix.

Full anatomy of a function
4 argument types
Common mistakes to watch for
Real code from YouTube, Amazon, Uber

This guide covers every concept behind Python functions in detail, verified against how Python actually runs, with real examples from YouTube, Amazon, Uber, and Google.


The Anatomy of a Function

Every function is built from the same set of parts:

   def   maximum ( num1, num2 ) :
   │      │        │              │
   │      │        │              └─ colon: ends the function header
   │      │        └─ parameters: the inputs the function accepts
   │      └─ function name: how you'll call it later
   └─ keyword that starts every function definition

       """Returns whichever number is larger."""   ← docstring (optional)

       if num1 > num2:
           return num1                              ← the actual logic
       else:
           return num2                              ← return sends a value back out
Part Required? What it does
def Yes Marks the start of a function definition
Function name Yes How you'll refer to and call the function later
Parameters No The inputs the function accepts; a function can take zero
: Yes Ends the header line
Docstring No A short description of what the function does
Body (statements) Yes The actual logic that runs
return No Sends a value back to wherever the function was called from

A function needs either return or print to actually be useful, but strictly speaking, neither is syntactically required; a function with no return simply hands back None.

Definition vs. Call: Two Separate Events

Writing a function and running it are two completely different steps, and it's worth seeing that distinction directly:

python
print('A function is defined.')

def maximum(num1, num2):
    if num1 > num2:
        print(num1, 'is greater than', num2)
    else:
        print(num2, 'is greater than', num1)

print('The function has been defined but not called yet.')

maximum(76, 22)
A function is defined.
The function has been defined but not called yet.
76 is greater than 22

Notice the code inside maximum never runs during the def block itself; Python just registers that the function exists. Nothing inside it executes until the moment maximum(76, 22) is actually called, several lines later.

   def maximum(...):        ← DEFINITION: Python memorizes the recipe
       ...                     but doesn't cook anything yet

   maximum(76, 22)          ← CALL: NOW the recipe actually runs,
                                using 76 and 22 as the ingredients

Parameters vs. Arguments

These two words get used interchangeably in casual conversation, but they mean specific, different things:

  • A parameter is the name written inside the parentheses in the function definition: num1 and num2 in def maximum(num1, num2):.
  • An argument is the actual value you pass in when you call the function: the 76 and 22 in maximum(76, 22).

The parameter is the labeled placeholder; the argument is what actually fills it in.


Types of Arguments

Python gives you three different ways to pass arguments into a function, and they can be mixed.

Positional Arguments

The default behavior: arguments are matched to parameters purely by their order.

python
maximum(76, 22)   # 76 becomes num1, 22 becomes num2, because of POSITION alone

Keyword Arguments

You can name which parameter each value belongs to, which frees you from having to match the original order:

python
maximum(num2=43, num1=99)   # order no longer matters: explicitly labeled
   Positional:  maximum(76, 22)
                          │   │
                    num1 ─┘   └─ num2      (matched by POSITION)

   Keyword:     maximum(num2=43, num1=99)
                         │            │
                    num2=43      num1=99   (matched by NAME, any order)

Default Arguments

You can give a parameter a fallback value in the function's definition, using =. If the caller doesn't supply that argument, the default is used instead:

python
def maximum(num1, num2=0):
    if num1 > num2:
        print(num1, 'is greater than', num2)
    else:
        print(num2, 'is greater than', num1)

maximum(43)        # num2 falls back to its default, 0
maximum(43, 99)    # the default is overridden with 99
43 is greater than 0
99 is greater than 43

One rule to keep in mind: once a parameter has a default value, every parameter after it must also have one. Writing def maximum(num1=0, num2): looks reasonable but isn't valid Python; it raises SyntaxError: parameter without a default follows parameter with a default. Python needs this rule so it can always tell, unambiguously, which positional arguments fill in which parameters. That's why the default-valued parameter goes last, as shown above.

Arbitrary Arguments (*args)

Sometimes you don't know in advance how many arguments will be passed in. Prefixing a parameter with * collects any number of extra positional arguments into a single tuple:

python
def add(*num):
    total = 0
    for value in num:
        total += value
    return total

add(34, 32, 98, 90, 55, 67)   # 376: works with 6 arguments, or 2, or 20

Inside the function, num is just a tuple holding whatever was passed in, so the for loop works exactly like it would over any other tuple.


Built-in Functions vs. User-Defined Functions

Every function you've written yourself so far, maximum, add, is a user-defined function. Python also ships with a large set of built-in functions that are simply already there for you to use: print(), len(), input(), id(), and type() are all examples you've likely already used without writing them yourself. The full list lives in the official Python documentation. The distinction is purely about who wrote it; both kinds work exactly the same way once called.


Functions With and Without return

A function can communicate its result in two very different ways.

python
def maximum(num1, num2):
    if num1 > num2:
        print(num1, 'is greater than', num2)
    else:
        print(num2, 'is greater than', num1)
    return None

This version displays the answer with print, but hands back None to the program, since there's no explicit value after return. Compare that with a version that actually returns something usable:

python
def greater(num1, num2):
    if num1 > num2:
        result = num1
    else:
        result = num2
    return result

returned_value = greater(99, 21)   # returned_value now holds 99

The difference matters more than it looks like it should: a print-only function shows you the answer once, on screen, and that's the end of it. A function that returns its answer hands you back an actual value you can store in a variable, pass to another function, or use in a calculation later in the program.

A typo worth watching for: it's easy to write a line like this and forget the f prefix:

python print('The maximum of two given numbers {num1} and {num2} is', greater(num1, num2))

Without f, curly braces are just ordinary characters to Python, not a signal to substitute a variable's value, so {num1} and {num2} print completely literally instead of showing the actual numbers. The fix is one letter:

python print(f'The maximum of two given numbers {num1} and {num2} is', greater(num1, num2))

This one is worth watching for specifically because it fails silently: no error, just wrong output.


Scope and Lifetime of Variables

Scope is where in your program a variable can be seen and used. Lifetime is how long it continues to exist in memory. Variables created inside a function have both limited: they're local to that function, and they're destroyed the moment the function finishes running.

python
val = 50

def greater(num1, num2):
    if num1 > num2:
        result = num1
    else:
        result = num2
    return result

greater(999.45, 334.32)

print(result)   # NameError: name 'result' is not defined
print(val)      # 50: val was never inside the function, so it's unaffected
   Outside the function              Inside greater()
   ┌─────────────────┐               ┌─────────────────┐
   │ val = 50         │               │ result = ...     │
   │ (stays alive     │               │ (created fresh   │
   │  the whole time) │               │  each call,      │
   │                  │               │  destroyed the   │
   │                  │               │  instant the     │
   │                  │               │  function ends)  │
   └─────────────────┘               └─────────────────┘

   print(result)  ← ERROR: result never existed out here
   print(val)     ← fine: val was always out here

result was created fresh inside greater, handed back via return, and then discarded the instant the function finished. Trying to reach it from outside afterward is like trying to walk into a room that was demolished the moment you left it.


Real-World Examples

YouTube: Engagement Score (Positional Arguments). Ranking millions of videos means running the same formula over and over, which is exactly what a function is for. The three inputs (views, likes, comments) are always supplied in the same fixed order.

python
def calculate_engagement(views, likes, comments):
    score = views + (likes * 2) + (comments * 5)
    return score

video_a_score = calculate_engagement(10000, 500, 50)
video_b_score = calculate_engagement(5000, 800, 120)

print(f"Video A Engagement Score: {video_a_score}")
print(f"Video B Engagement Score: {video_b_score}")
Video A Engagement Score: 11250
Video B Engagement Score: 7200

Amazon: Checkout Calculator (Keyword Arguments). With four separate dollar amounts going into one function, keyword arguments remove any risk of accidentally swapping two of them.

python
def calculate_checkout_total(subtotal, tax_rate, discount, shipping):
    tax_amount = subtotal * tax_rate
    total = subtotal + tax_amount - discount + shipping
    return total

final_price = calculate_checkout_total(
    shipping=5.00,
    discount=10.00,
    subtotal=100.00,
    tax_rate=0.08
)

print(f"Your final Amazon total is: ${final_price:.2f}")
Your final Amazon total is: $103.00

Uber: Fare Estimator (Positional + Default Arguments). Most riders just want the standard option, so ride_type defaults to "UberX" and only needs to be supplied when a rider picks something else.

python
def estimate_fare(distance_km, ride_type="UberX"):
    base_fare = 5.00
    per_km_rate = 2.00
    estimate = base_fare + (distance_km * per_km_rate)
    if ride_type == "UberXL":
        estimate *= 1.5
    elif ride_type == "Black":
        estimate *= 2.5
    return estimate

print(f"Standard UberX: ${estimate_fare(10):.2f}")
print(f"Large group (UberXL): ${estimate_fare(10, 'UberXL'):.2f}")
print(f"Luxury (Uber Black): ${estimate_fare(10, 'Black'):.2f}")
Standard UberX: $25.00
Large group (UberXL): $37.50
Luxury (Uber Black): $62.50

Google: Search Query Cleanup (Helper Functions). Breaking one task into two small functions, one that cleans text and one that runs the search, is a "helper function" pattern: each function does exactly one narrow thing, and the second function calls the first.

python
def clean_search_query(raw_query):
    cleaned = raw_query.strip().lower()
    return cleaned

def execute_google_search(user_input):
    print(f"Raw input received: '{user_input}'")
    search_term = clean_search_query(user_input)
    print(f"Searching database for: '{search_term}'...")
    print("Returning 10,000 results.\n")

execute_google_search("   MaCHinE LeaRning bAsiCs   ")
execute_google_search("  HOW to WRite a FUnctioN  ")
Raw input received: '   MaCHinE LeaRning bAsiCs   '
Searching database for: 'machine learning basics'...
Returning 10,000 results.

Raw input received: '  HOW to WRite a FUnctioN  '
Searching database for: 'how to write a function'...
Returning 10,000 results.

Quick Reference Summary

Concept Syntax What it does
Definition def name(params): Registers a reusable block of code
Call name(arguments) Actually runs the code inside the function
Parameter Written in the def line The labeled placeholder for an input
Argument Written in the call The actual value supplied for a parameter
Positional argument func(a, b) Matched to parameters by order
Keyword argument func(b=2, a=1) Matched to parameters by name, any order
Default argument def func(a, b=0): Falls back to a value if the caller omits it (must come after all non-default parameters)
Arbitrary arguments def func(*args): Collects any number of extra positional arguments into a tuple
return return value Sends a value back to the caller, usable elsewhere in the program
No return (nothing, or return alone) The function returns None
Local scope Any variable created inside a function Only exists, and is only visible, inside that function
Built-in function print(), len(), type()... Ships with Python; no need to define it yourself

Definition and call are two separate moments; parameters and arguments are two separate things; and a variable created inside a function has a life that begins and ends with that single call. Once those three distinctions are solid, the different argument styles (positional, keyword, default, arbitrary) stop looking like separate rules to memorize and start looking like different answers to the same question: how should this particular input reach the function?

Definition and call are two separate moments; parameters and arguments are two separate things. Once those distinctions are solid, positional, keyword, default, and arbitrary arguments stop looking like separate rules to memorize and start looking like different answers to the same question: how should this input reach the function?