Skip to content

Python Functions & Scope

A function is a named, reusable block of code. You define it once and call it as many times as you need. Functions keep programs organized, avoid repetition, and make code easier to test.

ConceptSyntaxExample
Definedef name(params):def greet(name):
Callname(args)greet('Alice')
Return a valuereturn valuereturn a + b
Default valueparam=valuedef greet(name, greeting='Hello')
Keyword argumentname=value at the callgreet('Bob', greeting='Hi')
Variable positional args*argsCollected into a tuple
Variable keyword args**kwargsCollected into a dict
Modify an outer variableglobal x, nonlocal xSee Scope

Built-in Functions Built-In

Section titled “Built-in Functions ”

Python ships with many functions that are always available, with no import needed.

FunctionPurposeExample
print(...)Display outputprint('a', 'b', sep='-') prints a-b
input(prompt)Read a line of text from the userinput('Name: ') returns a string
len(x)Number of itemslen('hello') gives 5
type(x)The type of a valuetype(3.5) gives <class 'float'>
int(x)Convert to an integerint(3.14) gives 3, int('42') gives 42
float(x)Convert to a floatfloat('7.8') gives 7.8
str(x)Convert to a stringstr(10) gives '10'
bool(x)Convert to a Booleanbool('') gives False
round(x, n)Round a numberround(4.253, 1) gives 4.3
abs(x)Absolute valueabs(-15) gives 15
min(), max(), sum()Smallest, largest, totalmax(4, 9, 2) gives 9
sorted(x)A new sorted listsorted([3, 1, 2]) gives [1, 2, 3]
range(n)A sequence of integerslist(range(3)) gives [0, 1, 2]

int() also accepts Booleans: int(True) gives 1 and int(False) gives 0. Use help(function_name) in a Python shell to read any function’s documentation.

input() always returns a string, even if the user types digits. Convert it explicitly when you need a number.

user_input.py
name = input('Name: ') # 'Alice'
age = int(input('Age: ')) # '30' becomes 30
try:
age = int(input('Age: '))
except ValueError:
print('Please enter a whole number') # Runs if the user types 'abc'

Use the def keyword, a name, parentheses with parameters, and a colon. The indented block beneath it is the function body.

function_definitions.py
def calculate_sum(a, b): # a and b are parameters
"""Return the sum of a and b."""
return a + b
result = calculate_sum(3, 1) # 3 and 1 are arguments
print(result) # 4
  • Parameters are the placeholder names in the function definition.
  • Arguments are the actual values passed in when the function is called.
  • The docstring (a triple-quoted string on the first line) documents the function and appears in help().

Name functions with snake_case verbs that describe what they do (calculate_tax, send_email). Type hints document the expected types, but Python does not enforce them at runtime.

type_hints.py
def calculate_sum(a: int, b: int) -> int:
return a + b

Arguments can be passed by position or by name. Positional arguments must come first.

argument_styles.py
def describe(name, age, city):
return f'{name}, {age}, {city}'
print(describe('Ann', 30, 'Paris')) # Positional
print(describe(age=30, city='Paris', name='Ann')) # Keyword, any order
print(describe('Ann', city='Paris', age=30)) # Mixed, positional first
describe(name='Ann', 30, 'Paris') # SyntaxError: positional argument follows keyword argument

A function call must supply a value for every required parameter, and no more than the function accepts.

arity_errors.py
calculate_sum() # TypeError: calculate_sum() missing 2 required positional arguments: 'a' and 'b'
calculate_sum(1, 2, 3) # TypeError: calculate_sum() takes 2 positional arguments but 3 were given

A parameter with a default value becomes optional. Parameters with defaults must come after all required parameters.

default_parameters.py
def greet(name, greeting='Hello'):
return f'{greeting}, {name}!'
print(greet('Alice')) # Hello, Alice!
print(greet('Bob', greeting='Welcome')) # Welcome, Bob!
def broken(greeting='Hello', name): # SyntaxError: a required parameter follows a default one
pass

When the number of arguments is not known in advance, use the unpacking operators in the definition.

  • *args collects extra positional arguments into a tuple.
  • **kwargs collects extra keyword arguments into a dictionary.
arbitrary_arguments.py
def sum_all(*args):
print(args) # (1, 2, 3, 4), a tuple
return sum(args)
print(sum_all(1, 2, 3, 4)) # 10
def print_profile(**kwargs):
print(kwargs) # {'name': 'Alex', 'role': 'Admin'}
print(type(kwargs)) # <class 'dict'>
print_profile(name='Alex', role='Admin')

The same operators work at the call site to unpack a collection into arguments:

unpacking_arguments.py
numbers = [1, 2, 3]
profile = {'name': 'Alex', 'role': 'Admin'}
print(sum_all(*numbers)) # 6, same as sum_all(1, 2, 3)
print_profile(**profile) # same as print_profile(name='Alex', role='Admin')

Any parameter placed after * or *args can only be passed by name. This makes calls self-documenting.

keyword_only.py
def connect(host, *, timeout=10):
return f'Connecting to {host} (timeout {timeout}s)'
print(connect('example.com', timeout=5)) # Connecting to example.com (timeout 5s)
print(connect('example.com', 5)) # TypeError: connect() takes 1 positional argument but 2 were given

When combining parameter kinds in one definition, use this order:

OrderKindExample
1Required positionallevel
2Defaultssep=' '
3*args*messages
4Keyword-onlyend='\n'
5**kwargs**extras

A function ends as soon as it reaches return, and the value after it is passed back to the caller.

return_behavior.py
def implicit_example(a, b):
print(a + b) # Displays 4, but returns nothing
output_1 = implicit_example(3, 1)
print(output_1) # None
def explicit_example(a, b):
return a + b # Hands the value back to the caller
output_2 = explicit_example(3, 1)
print(output_2) # 4

If a function finishes without hitting a return, it returns None NoneType. None represents “no value”, is falsy, and is a single shared object, so test for it with is None rather than ==.

print() displays a value to the screen. return gives a value back to the code that called the function. Only a returned value can be used in further calculations.

print_vs_return.py
def double_return(n):
return n * 2
def double_print(n):
print(n * 2)
print(double_return(5) + 1) # 11
print(double_print(5) + 1) # Prints 10, then TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

Because return exits immediately, you can use it to handle special cases first and keep the main logic flat. To return several values, separate them with commas. Python packs them into a tuple.

early_and_multiple_returns.py
def classify(age):
if age < 0:
return 'Invalid'
if age >= 18:
return 'Adult'
return 'Minor'
def min_max(numbers):
return min(numbers), max(numbers)
low, high = min_max([3, 1, 2]) # Unpacks the returned tuple: 1 and 3

Python passes references to objects. What that means in practice depends on whether the object is mutable.

argument_passing.py
def add_one(items):
items.append(1) # Mutates the caller's list
def reassign(items):
items = [99] # Rebinds the local name only
data = []
add_one(data)
print(data) # [1], the caller's list was changed
reassign(data)
print(data) # [1], unchanged

Scope defines where a variable can be seen and used. When Python meets a name, it searches four levels in order, known as the LEGB rule:

OrderScopeWhere it lives
1LocalInside the current function, including its parameters
2EnclosingInside any outer function that wraps the current one
3GlobalAt the top level of the module
4Built-inPython’s own names, such as len and print
scope_resolution.py
tax_rate = 0.1 # Global variable
def calculate_tax(price):
tax = price * tax_rate # price and tax are local; tax_rate is read from the global scope
return tax
print(calculate_tax(50)) # 5.0
print(tax_rate) # 0.1
print(tax) # NameError: name 'tax' is not defined

Local variables are created when the function starts and destroyed when it ends, so they cannot be reached from outside.

Assigning to a name inside a function creates a new local variable, even if a global with the same name exists. The global is untouched.

shadowing.py
x = 'global'
def show():
x = 'local'
print(x) # local
show()
print(x) # global

Reading a global works automatically. To reassign one from inside a function, declare it with global State Mutation.

global_keyword.py
counter = 0
def broken_increment():
counter += 1 # UnboundLocalError: Python treats counter as local because it is assigned
def increment():
global counter # Bind the name to the global variable
counter += 1
increment()
print(counter) # 1

Overusing global makes code hard to follow and test. Prefer passing values in and returning results out:

counter = 0
def increment():
global counter
counter += 1
increment()

nonlocal does for enclosing functions what global does for the module. It is used with nested functions, commonly to keep private state between calls.

nonlocal_keyword.py
def make_counter():
count = 0
def increment():
nonlocal count # Refers to count in make_counter()
count += 1
return count
return increment
counter = make_counter()
print(counter()) # 1
print(counter()) # 2

Functions are objects. You can store them in variables and pass them to other functions, which is how key= arguments work. Pass the function without parentheses: parentheses would call it immediately.

functions_as_values.py
def shout(text):
return text.upper()
greeter = shout
print(greeter('hi')) # HI
words = ['banana', 'kiwi', 'apple']
print(sorted(words, key=len)) # ['kiwi', 'apple', 'banana']
print(sorted(words, key=lambda w: w[-1])) # ['banana', 'apple', 'kiwi'], by last letter
square = lambda n: n ** 2 # A lambda is a tiny one-expression function
print(square(4)) # 16

  1. Name it with a verb. calculate_tax tells the reader what happens. tax does not.

  2. Decide the inputs. Make required values parameters, give sensible defaults to optional ones, and avoid relying on globals.

  3. Do one job. If a function is hard to name or needs “and” in its description, split it into two.

  4. Return results instead of printing them. Returned values can be tested, reused, and combined. Reserve print() for functions whose purpose is output.

  5. Test the edge cases. Try empty input, zero, negative numbers, and the wrong type.


MistakeProblemFix
Calling greet instead of greet()Without parentheses the function is not runAdd parentheses and arguments
print() instead of returnThe caller receives NoneReturn the value
def f(items=[])The default list is shared across callsdef f(items=None)
counter += 1 inside a functionUnboundLocalErrorUse global, or better, a parameter and return value
age = input('Age: ') then age + 1input() returns a stringint(input('Age: '))
Required parameter after a defaultSyntaxErrorPut required parameters first
Positional argument after a keyword argumentSyntaxErrorPositional first, keywords last
Using a local variable outside its functionNameErrorReturn it, or declare it at the right scope
key=len()Calls len with no argumentskey=len
result = my_list.sort()Returns NoneCall my_list.sort() alone, or use sorted()
  1. Give each function a single, clear purpose and a descriptive verb name.
  2. Add a docstring, and type hints where they help readers.
  3. Return values rather than printing them.
  4. Never use mutable objects as default arguments.
  5. Pass data in through parameters instead of reading or writing globals.
  6. Use keyword-only parameters (after *) for options that should be named at the call site.
  7. Use *args and **kwargs sparingly, and only when the function genuinely needs flexible arity.
  8. Test for None with is None.