Skip to content

Programming Fundamentals & OOP

Functions and Scope

A function bundles a sequence of statements under a name, optionally accepts input through parameters, and optionally sends a result back with return. Scope is the set of names visible at a given point in the program - a function's own local names, any enclosing function's names, and the module's global names, roughly in that order of preference. Together, functions and scope are how a program stays organized as it grows past the length of a single script.

Why it matters

Repeated logic in one place is logic you only have to fix once
A bug fixed inside a function is fixed everywhere that function is called; the same bug copy-pasted five times has to be found and fixed five times.
Function boundaries are the natural unit for testing
A well-scoped function can be called directly with known inputs and checked against a known output, without running the rest of the program around it.
Scope rules prevent name collisions across a large codebase
Two functions can both use a variable named total internally without interfering with each other, because each has its own local scope.
A function's signature is a contract other code relies on without reading the implementation
Parameters and a return value tell a caller what to provide and what to expect back, which is what makes calling a library function possible without reading its source.

Defining and calling a function

A function is defined once, with def in Python, naming its parameters and the block of code that runs when it is called. Parameters can have default values, which make them optional at the call site; calling the function with fewer arguments than it has required parameters is a runtime error, not a warning. return sends a value back to the caller and immediately ends the function - code written after a return in the same branch never runs.

Python
def greet(name, greeting='Hello'):
    return f'{greeting}, {name}!'

print(greet('Priya'))          # Hello, Priya!
print(greet('Priya', 'Hi'))    # Hi, Priya!

Scope: local, enclosing, and global

A name created inside a function is local to that function - it does not exist before the function is called and disappears after it returns. A name defined at the top level of a module is global, and is visible inside any function in that module, but a function cannot assign to a global name without explicitly declaring it with the global keyword; without that, assigning to the name inside the function just creates a new local name that shadows the global one. Python's scope is per-function, not per-block: a variable created inside an if or for block is visible for the rest of the enclosing function, unlike in languages such as Java or C, where a block introduces its own scope.

Python
count = 0

def increment():
    count = count + 1   # UnboundLocalError: count is treated as local here
    return count

def increment_fixed():
    global count
    count = count + 1
    return count

How arguments actually get passed

Python passes arguments by object reference: the parameter name inside the function refers to the same object the caller's argument referred to. If that object is mutable and the function changes its contents, such as appending to a list, the caller sees the change, because there was only ever one list. If the function instead reassigns the parameter to a new value, that only rebinds the local name - the caller's variable still points at the original object. This single rule explains a large share of the confusion beginners have about functions 'not returning the right thing.'

Mistakes people make here

Giving a function a mutable default argument
A default value is created exactly once, when the function is defined, not once per call - so every call that relies on a mutable default like an empty list shares and mutates the same object, and values from one call leak into the next. Use None as the default and create a new list inside the function body instead.
Assuming reassigning a parameter changes the caller's variable
Reassigning a parameter inside a function only changes what the local name refers to; it never changes what the caller's variable refers to. Only mutating a shared mutable object, not reassigning it, is visible to the caller.
Shadowing an outer name by reusing it as a parameter name
A parameter that reuses a global variable's name silently hides the outer meaning for the rest of the function body, which can produce confusing bugs far from where the shadowing actually happened.
Forgetting a return statement and assuming the function's last expression is returned automatically
Unlike some languages, Python does not implicitly return the last evaluated expression. A function with no return statement returns None, and using that None as if it were a real result fails somewhere downstream, often far from the actual mistake.

Strengths and trade-offs

Where it is strong

  • Turns duplicated logic into one place to read, test, and fix.
  • A clear function signature documents intent - its expected inputs and output - better than a comment above old copy-pasted code ever did.
  • Small, well-scoped functions can be tested in isolation, without standing up the rest of the program around them.

The trade-offs

  • Overusing tiny, single-line functions purely for the sake of decomposition can make the overall control flow harder to trace, not easier, because the reader now has to jump between definitions.
  • Relying on global state accessed from many functions defeats most of the isolation benefit scope is supposed to provide, and reintroduces the exact bugs scoping was meant to prevent.
  • Deciding what should be a parameter versus a class attribute versus a genuine global is a real design judgment call - scope rules enforce visibility, but they do not make that decision for you.

Who needs this

Everyone, as soon as a script grows past roughly the length of a single task. It is also one of the first places a beginner's mental model needs correcting, since a function changing a list but not a number is confusing until the pass-by-object-reference rule actually clicks.

Questions about functions and scope

What's the difference between a parameter and an argument?
A parameter is the name used inside the function definition; an argument is the actual value supplied at the call site. def greet(name) has a parameter called name; calling greet with an actual value passes that value as the argument.
Why did my function change my list argument even though I never used return?
Because Python passes the reference to the list, not a copy of it. Any in-place mutation inside the function, such as append, sort, or item assignment, changes the same list object the caller has, with or without a return statement. Only reassigning the parameter to a brand-new list would not be visible to the caller.
What is scope, in one sentence?
Scope is the answer to which names are visible from this exact line of code, and Python resolves it in local, then enclosing function, then global, then built-in order.
Do all languages pass function arguments the same way?
No. Some languages copy the value (pass by value), some pass a reference the callee can even reassign for the caller (pass by reference), and Python's model, passing a reference to the object but letting reassignment only affect the local name, is its own distinct middle ground worth learning specifically, not assuming from another language.

The primary source

Related concepts

← All concept guides