BeginnerPython · 37 min · 17 challenges

Functional Programming: Beginner

Databricks processes petabytes of data daily using functional patterns where map() applies the same transformation to millions of records simultaneously, and filter() removes unwanted rows without a single for loop. The data engineering teams at companies like Instacart use these tools to clean and reshape raw data and to validate it, in pipelines that run thousands of times per day. lambda functions let you define a transformation inline without naming it, so pipeline code stays compact. This lesson covers map() and filter() first and then lambda; most data transformation pipelines are built from those patterns.

Defining Functions

Daily Life
Interviews
A function is a named block of code you can run from anywhere in a program, so the same logic never has to be written twice.

Calling a function

Every time you write print("Hello") or len("text"), you're calling a function. These are built-in functions that come with Python, created by Python developers to handle common tasks. The print() function displays output to the screen. The len() function calculates the length of a string or collection. You do not need to know how these functions work internally; you just need to know what to give them and what they give back.

# Using built-in functions you already know
message = "Hello, Python!"
print(message)
print(len(message))
# More built-in functions
numbers = [5, 2, 8, 1, 9]
print(max(numbers))
print(min(numbers))
print(sum(numbers))
>>>Output
Hello, Python!
14
9
1
25

Each function call follows the same pattern: write the function name, open parentheses, provide any data the function needs (called arguments), and close parentheses. The function processes the arguments and may produce a result you can use.

01
Function Name
Identifies which function to run
02
Parentheses ()
Signal that you're calling the function
03
Arguments
Values inside the parentheses provide input data
04
Return Value
The function may send a result back for you to store or use

Functions for your own rules

Built-in functions cover common operations, but your programs have unique requirements. You might need to calculate taxes for your specific business rules, format names according to your company standards, or validate data in ways Python can't predict. Creating your own functions lets you package this custom logic and reuse it throughout your program.
Consider a program that needs to calculate the area of rectangles in several places. Without functions, you would repeat the calculation each time:
# First calculation
width1 = 5
height1 = 3
area1 = width1 * height1
print("Room 1 area:", area1)
width2 = 10
height2 = 4
area2 = width2 * height2
print("Room 2 area:", area2)
width3 = 7
height3 = 6
area3 = width3 * height3
print("Room 3 area:", area3)
The calculation width * height appears 3 times. That looks harmless until the formula changes, say to add a margin for waste or to convert units, and you have to update every place you copied it. A function holds the logic in one place, and you call it from anywhere.
Reusability
Reusability
Write code once and call it from anywhere in your program
Maintainability
Maintainability
Fix bugs in a single place instead of hunting down every copy
Readability
Readability
Give complex logic a meaningful name that explains its purpose
Testability
Testability
Test each function independently to catch errors early
Organization
Organization
Break large problems into smaller, manageable pieces

To create a function, use the def keyword (short for "define") followed by the function name with parentheses after it, and end the line with a colon. The lines of code that belong to the function must be indented. This indented block is called the function body, and it contains the code that runs when you call the function.

# Define a simple function
def greet():
print("Hello, World!")
print("Welcome to Python functions!")
print("Before calling the function")
greet()
print("After calling the function")
>>>Output
Before calling the function
Hello, World!
Welcome to Python functions!
After calling the function

Notice the order of output. The code inside the function only runs when you call it with greet(). You can call a function as many times as you want, and each call executes the function body from the beginning.

def announce():
print("*" * 20)
print("IMPORTANT MESSAGE")
print("*" * 20)
announce()
print("First message")
announce()
print("Second message")
announce()
>>>Output
********************
IMPORTANT MESSAGE
********************
First message
********************
IMPORTANT MESSAGE
********************
Second message
********************
IMPORTANT MESSAGE
********************

Parts of a function definition

Every function definition has the same structure, and knowing the parts makes your own functions easier to get right and other people's code easier to read:
def function_name():
# Function body
# Indented
statement_1
statement_2
statement_3
# Outside function
def
Keyword
Tells Python: new function
name
Identifier
How you call the function
()
Parentheses
Required, even if empty
:
Colon
Ends the function header
body
Indented Block
Runs when function called

Function naming conventions

Function names follow the same rules as variable names. They must start with a letter or the _ character, and digits are allowed after that. Python keywords are off limits. On top of those rules, Python code follows strong conventions that keep names readable:
•Good function names
  • calculate_area
  • format_user_name
  • validate_email
  • convert_temperature
  • get_total_price
•Poor function names
  • doStuff
  • f
  • myFunction
  • process
  • x
Good function names use snake_case, which means all lowercase with _ between words. They use verbs or verb phrases that describe what the function does. Someone reading calculate_area immediately understands the purpose without looking at the code inside.
TIP
A function name should describe what it does, not how it does it. "calculate_tax" is better than "multiply_by_rate" because it describes the purpose, not the implementation detail that might change.

Indentation is mandatory

Python uses indentation to determine which code belongs to a function. Every line in the function body must be indented by the same amount (typically 4 spaces). Inconsistent indentation causes errors:
# CORRECT: Consistent indentation
def greet():
print("Hello!")
print("Welcome!")
print("Have a great day!")
# WRONG: Would cause IndentationError
def greet_broken():
print("Hello!")
# Error! Wrong indentation
print("This line has wrong indentation!")
print("Have a great day!")
When you reduce the indentation back to the original level, you signal the end of the function body. The next unindented line is outside the function and runs independently of any function call.
Most of any professional Python codebase is function definitions and the calls that combine them.
Python uses indentation to mark the function body, and that body runs only when you call the function, not when you define it.
Prefer calculate_tax to multiply_by_rate, because the first name still fits if the tax calculation changes and the second exposes an implementation detail.

Function Parameters

Daily Life
Interviews
Most functions need input: an area function needs the dimensions, and a name formatter needs the name. Parameters are variables that receive values when you call the function. They appear inside the parentheses in the function definition and act as placeholders for the actual values you will provide.
Parameters and arguments get mixed up a lot. They describe the same value, seen once from the definition and once from the call.
Parameter
In Definition
Variable in the def line
Argument
In Call
Actual value you pass in
# name is a PARAMETER - receives a value
def greet(name):
print("Hello, " + name + "!")
print("Nice to meet you, " + name + ".")
# "Alice" is an ARGUMENT
greet("Alice")
print("---")
greet("Bob")
print("---")
greet("Charlie")
>>>Output
Hello, Alice!
Nice to meet you, Alice.
---
Hello, Bob!
Nice to meet you, Bob.
---
Hello, Charlie!
Nice to meet you, Charlie.

When you call greet("Alice"), Python assigns the value "Alice" to the parameter name. Inside the function, name behaves like any other variable, holding that value for the duration of the function call.

Multiple parameters

Functions can accept multiple parameters, separated by commas. When calling the function, you must provide arguments in the same order as the parameters are defined:
def calculate_area(width, height):
area = width * height
print("Width: " + str(width))
print("Height: " + str(height))
print("Area: " + str(area))
print("---")
# Arguments must match parameter order
calculate_area(5, 3)
calculate_area(10, 4)
calculate_area(7, 6)
>>>Output
Width: 5
Height: 3
Area: 15
---
Width: 10
Height: 4
Area: 40
---
Width: 7
Height: 6
Area: 42
---
The first argument becomes the first parameter, the second argument becomes the second parameter, and so on. This is called positional matching because the position determines which parameter receives which value.

Default parameter values

Sometimes you want a parameter to have a sensible default value that the caller can override if needed. This makes parameters optional. Default values are specified with an equals sign in the function definition:
def greet(name, greeting="Hello"):
print(greeting + ", " + name + "!")
# Using the default greeting
greet("Alice")
# Providing a custom greeting
greet("Bob", "Hi")
greet("Charlie", "Good morning")
greet("Diana", "Welcome")
>>>Output
Hello, Alice!
Hi, Bob!
Good morning, Charlie!
Welcome, Diana!

The parameter greeting has a default value of "Hello". When you call greet("Alice") with only one argument, greeting uses its default. When you provide a second argument, that value overrides the default.

TIP
Parameters with default values must come after parameters without defaults. Writing def greet(greeting="Hello", name) causes a syntax error because Python can't determine which argument goes where.

A formatting function with defaults

Defaults suit functions where most callers want the same behavior. In this formatting function, most calls use the defaults, and callers customize only when they need to:
def format_price(amount, currency="$", decimals=2):
formatted = round(amount, decimals)
return currency + str(formatted)
# Most common use - just the amount
print(format_price(19.99))
print(format_price(100))
# Override currency
print(format_price(50, "EUR "))
# Override both
print(format_price(1234.5678, "GBP ", 3))
>>>Output
$19.99
$100
EUR 50
GBP 1234.568
Callers who just need dollar formatting with 2 decimals pass only the amount. Callers with special needs can override one or both defaults. This pattern appears throughout Python's standard library.

Wrong number of arguments

If you call a function with too few or too many arguments, Python raises a TypeError. The error message tells you exactly how many arguments were expected versus how many you provided:
def greet(first_name, last_name):
print("Hello, " + first_name + " " + last_name + "!")
# Correct: provides both required arguments
greet("Ada", "Lovelace")
# Wrong: missing the second argument
# greet("Ada")
# TypeError: missing required argument
# Wrong: too many arguments
# greet("Ada", "Lovelace", "PhD")
# TypeError: takes 2 arguments but 3 given
The message names the function, how many arguments it expects, and how many you provided, which is usually enough to find the mismatch.
Try calling the function below with the defaults, then override them.
Well-chosen parameters let one function serve many callers without edits to its body.
Default values let callers omit arguments they do not need to customize, which cuts boilerplate in the common case.
Parameters without defaults are required; parameters with defaults are optional. Required parameters must always come before optional ones in the function signature so Python can unambiguously match arguments to parameters.

Return Values

Daily Life
Interviews

Functions can send data back to the caller using the return statement. This is how a function hands a result back for the caller to store or pass on to another function.

def calculate_area(width, height):
return width * height
# Store the returned value in a variable
area1 = calculate_area(5, 3)
area2 = calculate_area(10, 4)
print("First area: " + str(area1))
print("Second area: " + str(area2))
print("Combined area: " + str(area1 + area2))
# Use directly in expressions
total_cost = calculate_area(12, 8) * 25
print("Cost at $25 per sq unit: $" + str(total_cost))
>>>Output
First area: 15
Second area: 40
Combined area: 55
Cost at $25 per sq unit: $2400

The return statement immediately exits the function and sends the specified value back to where the function was called. That value replaces the function call in the expression, as if you had written the value directly.

Return versus print

print() and return get confused because both look like output, but only one of them hands the calling code a value it can use:

•print()
  • Displays text on screen
  • For human consumption only
  • Function returns None
  • Can't use the value later
  • Side effect, not a result
•return
  • Sends value back to caller
  • For program consumption
  • Value can be stored and used
  • Enables further calculations
  • Actual function result
def add_with_print(a, b):
# Shows the sum but returns nothing
print(a + b)
def add_with_return(a, b):
# Returns the sum for further use
return a + b
# Try to use the results
result1 = add_with_print(3, 5)
result2 = add_with_return(3, 5)
print("result1 is: " + str(result1))
print("result2 is: " + str(result2))
print("result2 doubled: " + str(result2 * 2))
>>>Output
8
result1 is: None
result2 is: 8
result2 doubled: 16

add_with_print displays 8 on the screen but returns None, so result1 is useless. add_with_return does not display anything but returns 8, which we can store and reuse in later calculations.

Functions without a return

If a function doesn't have a return statement, or if return is used without a value, the function automatically returns None. None is Python's special value representing "nothing" or "no value."

def say_hello():
print("Hello!")
# No return statement
def say_goodbye():
print("Goodbye!")
return
result1 = say_hello()
result2 = say_goodbye()
print("Result 1:", result1)
print("Result 2:", result2)
print("Both are None:", result1 is None and result2 is None)
>>>Output
Hello!
Goodbye!
Result 1: None
Result 2: None
Both are None: True
Functions without return values exist for their "side effects", such as printing messages or writing to files. Return a value when the caller needs one back; leave it out when the action itself is the function's job.

Return exits immediately

When Python encounters a return statement, it immediately exits the function. Any code after the return statement never runs. This is useful for early exits when you have already determined the result:

def check_age(age):
if age < 0:
return "Invalid: age cannot be negative"
if age < 18:
return "Minor"
if age < 65:
return "Adult"
return "Senior"
print(check_age(-5))
print(check_age(15))
print(check_age(30))
print(check_age(70))
>>>Output
Invalid: age cannot be negative
Minor
Adult
Senior
Each condition checks the age and returns as soon as one matches, so the function never runs more code than it needs to.

Scope and Variables

Daily Life
Interviews
Scope determines where a variable exists and can be accessed. Understanding scope prevents bugs where variables unexpectedly have wrong values or do not exist when you expect them to. Python has 2 main scopes relevant to functions: local scope (inside a function) and global scope (outside all functions).
Scope definition
  • Local scope - variables created inside a function, only visible there
  • Global scope - variables created outside all functions, visible everywhere
  • A variable's scope is determined by where it is created

Local variables

Variables created inside a function are local to that function. They're created when the function starts running and destroyed when the function finishes. They don't exist outside the function and can't be accessed from elsewhere:
def calculate():
result = 100
multiplier = 5
total = result * multiplier
print("Inside function:", total)
calculate()
# print(result) # NameError! result does not exist here
# print(multiplier) # NameError! multiplier does not exist here
# print(total) # NameError! total does not exist here
The isolation is deliberate. You can use the same variable name in different functions without conflict, because each function has its own local scope.
def function_one():
# x is local to function_one
x = 10
print("function_one x:", x)
def function_two():
# x is local to function_two, completely separate
x = 99
print("function_two x:", x)
function_one()
function_two()
function_one()
>>>Output
function_one x: 10
function_two x: 99
function_one x: 10

Global variables

Variables created outside any function are global. They exist for the entire program and can be read from inside functions. But you can't modify them from inside a function without special syntax (which we'll cover in the intermediate lesson).
TAX_RATE = 0.08
COMPANY_NAME = "ACME Corp"
def calculate_tax(price):
# Can READ global variables
tax = price * TAX_RATE
return tax
def print_receipt(item, price):
# Can READ globals too
tax = calculate_tax(price)
total = price + tax
print(COMPANY_NAME)
print("-" * 20)
print(item + ": $" + str(price))
print("Tax: $" + str(tax))
print("Total: $" + str(total))
print_receipt("Widget", 100)
>>>Output
ACME Corp
--------------------
Widget: $100
Tax: $8.0
Total: $108.0
Global variables named in ALL_CAPS indicate constants that should not change. Functions can read these values freely, so they are an easy way to share configuration across your program.

Parameters as local variables

Parameters are local variables too. When you call a function, Python creates local variables from the parameters and assigns the argument values to them. Modifying a parameter inside the function doesn't affect variables outside:
def attempt_to_modify(x):
print("Inside function, before:", x)
# Modifies the local x only
x = x * 2
print("Inside function, after:", x)
number = 5
print("Before function call:", number)
attempt_to_modify(number)
# Unchanged!
print("After function call:", number)
>>>Output
Before function call: 5
Inside function, before: 5
Inside function, after: 10
After function call: 5

The parameter x inside the function is a separate variable that happens to start with the same value as number. Changing x does not change number. This is called pass by value for simple types like numbers and strings.

TIP
This is usually what you want, because a function that doesn't modify external state is easier to test and debug. Use return values to send results back instead of modifying outside variables.

Scope lookup order

When you use a variable name, Python searches for it in order: first local scope, then enclosing scopes (for nested functions), then global scope, then built-in scope. It uses the first match it finds:
x = "global"
def outer():
x = "outer"
def inner():
x = "inner"
print("inner sees:", x)
inner()
print("outer sees:", x)
outer()
print("global sees:", x)
>>>Output
inner sees: inner
outer sees: outer
global sees: global
Each scope has its own x. The inner function finds its local x first and uses it, the outer function finds its own local x, and the global code finds the global x, so the 3 never interfere.
Local variables created inside a function are destroyed when the function returns, which keeps each call isolated.
Global variables are best used for true constants that don't change during the program, like configuration settings. Passing values through parameters and returning results through return values produces more predictable, testable functions.
Because Python checks local scope before global scope, a local variable with the same name as a global always shadows the global inside that function.

Type Hints and Documentation

Daily Life
Interviews
On a codebase that several people work on, you need to record what types of values each function expects and returns. Python's type hints are annotations for exactly that. They don't change how code runs; they're documentation for humans and tools.

Basic type hints

Add type hints after parameter names with a colon and the type. Add return type hints after the closing parenthesis with -> followed by the type:

def calculate_area(width: float, height: float) -> float:
return width * height
def greet(name: str) -> str:
return "Hello, " + name + "!"
def is_adult(age: int) -> bool:
return age >= 18
def print_message(message: str) -> None:
print(message)

These hints tell readers that calculate_area expects 2 float numbers and returns a float. greet expects a str and returns a str. is_adult expects an int and returns a bool (True or False).

Common type hints
  • int: Whole numbers like 5, -10, 0
  • float: Decimal numbers like 3.14, -2.5
  • str: Text strings like "hello"
  • bool: Boolean values True or False
  • None: The special "no value" value
  • list: A list of items
  • dict: A dictionary of key-value pairs

Type hints are not enforced

Python doesn't enforce type hints at runtime. You can pass a str where the hint says int, and Python will try to run the code. This differs from statically-typed languages like Java or TypeScript. Type hints are purely informational:

def double(x: int) -> int:
return x * 2
print(double(5))
print(double("ab"))
>>>Output
10
abab

The type hint says int, but Python happily multiplies a str by 2 (which repeats it). Type hints are for documentation and for external tools like type checkers (mypy) that analyze code before you run it.

Type hints pay off in larger codebases

The payoff grows with the size of the codebase and the number of people working in it.
Benefits of type hints
  • Documentation: Readers immediately know expected types
  • IDE support: Code editors provide better autocomplete
  • Error prevention: Type checkers catch bugs before runtime
  • Refactoring: Easier to change code with type information
  • Team communication: Clear contracts between functions
Write type hints from the start, so the habit is already in place when the codebase grows.
TIP
Many professional codebases require type hints for all function signatures. Using them in personal projects too keeps your signatures self-documenting and catches bugs early.
Outside the language itself, a lot of tooling has grown up around type hints.

Mistakes that come up with functions

These show up in everyone's code at some point, and each is quick to fix once you recognize it:

Forgetting to call the function

Defining a function doesn't run it. You must call the function with parentheses to execute its code:
•Wrong
  • def greet():
  • print("Hello!")
  • # Function defined but never called
  • # Nothing prints!
•Correct
  • def greet():
  • print("Hello!")
  • greet() # Call with parentheses
  • # Prints "Hello!"

Forgetting the return statement

If you want to use a function's result, you must return it. Calculating a value without returning it means the result is lost:
•Wrong
  • def add(a, b):
  • result = a + b
  • # Forgot return!
  • total = add(3, 5) # total is None
•Correct
  • def add(a, b):
  • return a + b
  • total = add(3, 5) # total is 8
Forgetting return is a very common bug. Try fixing the one below, where a function calculates a value and then loses it.

Calling before defining

Python executes code from top to bottom. A function must be defined before the line that calls it:
# greet() # NameError
def greet():
print("Hello!")
greet()

Confusing a reference with a call

greet (without parentheses) is the function object itself. greet() (with parentheses) calls the function. They are very different:

def greet():
return "Hello!"
print(greet)
print(greet())
>>>Output
<function greet at 0x...>
Hello!
❯❯❯PUTTING IT ALL TOGETHER

> You are a data analyst at Salesforce building a reusable ETL utility that cleans contact records across ten regional CSV files, each requiring the same standardization logic applied consistently without duplicating code.

def packages the record-cleaning logic once with a descriptive name so every regional file calls the same standardization routine without copy-pasting.
Function parameters take each regional dataset as an argument, so the same function body runs on different inputs without hardcoded file paths.
return passes the cleaned record list back to the caller so the result can be written to the output file or passed to the next pipeline stage.
Scope rules keep the cleaning function's counters and temporary strings local, so they can't overwrite variables in the outer pipeline script.
KEY TAKEAWAYS
Use def to define functions that package reusable code
Parameters receive values passed as arguments when calling
Default values (param=value) make parameters optional
return sends a value back; without it, the function returns None
Local variables exist only inside their function
Global variables can be read but not modified without the global keyword
Type hints document expected types but aren't enforced at runtime
Functions must be defined before the code that calls them

Building reusable blocks of logic

Category
Python
Difficulty
beginner
Duration
37 minutes
Challenges
17 hands-on challenges

Topics covered: Defining Functions, Function Parameters, Return Values, Scope and Variables, Type Hints and Documentation

Lesson Sections

  1. Defining Functions (concepts: Functions)

    A function is a named block of code you can run from anywhere in a program, so the same logic never has to be written twice. Calling a function Functions for your own rules Built-in functions cover common operations, but your programs have unique requirements. You might need to calculate taxes for your specific business rules, format names according to your company standards, or validate data in ways Python can't predict. Creating your own functions lets you package this custom logic and reuse i

  2. Function Parameters (concepts: Flexible Arguments)

    Most functions need input: an area function needs the dimensions, and a name formatter needs the name. Parameters are variables that receive values when you call the function. They appear inside the parentheses in the function definition and act as placeholders for the actual values you will provide. Parameters and arguments get mixed up a lot. They describe the same value, seen once from the definition and once from the call. Multiple parameters Functions can accept multiple parameters, separat

  3. Return Values (concepts: Functions)

    Return versus print Functions without a return Functions without return values exist for their "side effects", such as printing messages or writing to files. Return a value when the caller needs one back; leave it out when the action itself is the function's job. Return exits immediately Each condition checks the age and returns as soon as one matches, so the function never runs more code than it needs to.

  4. Scope and Variables (concepts: Scope)

    Scope determines where a variable exists and can be accessed. Understanding scope prevents bugs where variables unexpectedly have wrong values or do not exist when you expect them to. Python has 2 main scopes relevant to functions: local scope (inside a function) and global scope (outside all functions). Local variables Variables created inside a function are local to that function. They're created when the function starts running and destroyed when the function finishes. They don't exist outsid

  5. Type Hints and Documentation (concepts: Type Hints)

    On a codebase that several people work on, you need to record what types of values each function expects and returns. Python's type hints are annotations for exactly that. They don't change how code runs; they're documentation for humans and tools. Basic type hints Type hints are not enforced Type hints pay off in larger codebases The payoff grows with the size of the codebase and the number of people working in it. Write type hints from the start, so the habit is already in place when the codeb