5 Functions and Program Structure
Learn how Python functions organize programs, manage data flow, support reuse, and enable systematic testing and maintenance.
What Functions Do
Functions package instructions into named, reusable units. They help divide a complex problem into smaller tasks, reduce repeated code, and make programs easier to understand, test, and maintain.
A commonly has four parts:
A name that identifies its task.
Parameters that receive input values.
A body containing the instructions to execute.
A when it produces a result.
In Python, def introduces a definition, while a call runs the . A should usually have one clear responsibility. For example, a that calculates a total should not also display menus, read files, and update unrelated data.
A procedure is often described as a routine that performs an action without producing a useful value for its caller. The distinction depends on the programming language. In Python, every technically returns a value: an explicit return provides the specified result, while reaching the end without return produces None.
Takeaway: Use functions to give focused tasks clear names and predictable responsibilities.
Inputs and Calls
The distinction between a and an describes two sides of the same call. A is named in the definition; an is supplied by the caller.
For example, in greet("Maya"), name can be the and "Maya" is the . A may accept multiple parameters, default values, and keyword arguments. Default values make some arguments optional, while keyword arguments associate values explicitly with names.
Keep parameters focused and limited. Many unrelated parameters may indicate that a is doing too many jobs. When a receives a mutable object such as a list, operations inside the may modify that object. Returning a new result is often easier to predict than changing shared data unnecessarily.
Takeaway: Define inputs clearly, use descriptive names, and make changes to shared data explicit.
Return Values and Data Flow
A is the output of a . Returning a value allows the caller to store it, compare it, display it, or pass it to another . Printing a value only displays it; it does not provide that value to the caller in the same way.
For instance, a that returns a doubled number can have its result assigned to a variable. A that only prints the doubled number displays output, but an assignment from that call receives None.
A can have several return paths, but each path should produce a meaningful and consistent kind of result whenever possible. The return statement also ends the current immediately, so instructions after an unconditional return do not execute.
Return values support explicit data flow. A caller can use a returned result in a condition, combine it with other results, or send it to another without relying on hidden state.
Takeaway: Prefer explicit results through return values over communication through printing or hidden changes.
, Namespaces, and
determines where a name can be accessed. A local variable is created inside a and normally cannot be used outside that . A global variable is defined at the module or program level and may be accessible from several functions.
Python creates a local for each call. If a assigns message locally while a separate message exists outside the , the two names refer to separate bindings. The local assignment does not automatically change the global value.
Local variables support by protecting a 's internal state from unrelated parts of the program. Global variables should be used sparingly because global state can create hidden dependencies. A may appear to depend only on its parameters while actually reading or changing data elsewhere.
A clearer design passes required data as parameters and returns results explicitly. This makes dependencies visible to callers and makes the easier to test.
Takeaway: Keep internal state local when possible and make a 's required data and results visible through its .
and
divides a program into independent, understandable components. A component may be a , class, file, or package, and each component should have a clear purpose.
An describes how a component can be used. It includes:
The component's name.
The parameters it accepts.
The values or errors it can produce.
Important side effects, such as writing a file or changing a database.
Good aims for , , clear interfaces, and information hiding. means that a component's responsibilities are closely related. means that components depend on one another as little as practical. Information hiding keeps internal details inside the component that owns them.
For example, a grade-report program can use one to calculate an average, another to determine a letter grade, and a third to assemble the report. Changing the grading scale then requires changing the grading rather than rewriting the average calculation or report assembly.
strengthens . Instead of copying the same calculation into several places, define a descriptive and call it wherever the related behavior is needed. Reuse reduces duplication and helps ensure that a correction is applied consistently.
Takeaway: Build components with focused responsibilities, explicit interfaces, and minimal unnecessary dependence on other components.
Decomposing Problems
turns a large problem into a sequence of smaller tasks. A practical process is:
State the overall problem.
Identify the major tasks needed to solve it.
Create one for each focused task.
Define the data each receives and returns.
Connect the functions in a main algorithm.
Test each independently, then test the complete program.
A shopping program, for example, might separate reading items, calculating a subtotal, calculating a discount, calculating tax, and formatting a receipt. If the final total is wrong, each stage can be checked separately instead of debugging one large block of code.
A should generally be short enough that its purpose and control flow can be understood without examining the entire program. Shorter is not automatically better, however. Splitting a simple operation into many meaningless functions can make a program harder to follow.
Takeaway: Decompose problems according to meaningful responsibilities, not merely according to line count.
Testing and Reliable Maintenance
Testing checks whether a program produces expected results for selected inputs. A commonly follows four steps:
Setup: Prepare the input or required objects.
Execution: Call the .
Verification: Compare the actual result with the expected result.
Cleanup: Remove temporary resources when necessary.
Choose test inputs that represent important behaviors:
Typical values for ordinary cases.
Boundary values at or near a limit.
Empty values when empty input is permitted.
Large values that may reveal performance or overflow problems.
Invalid values that should produce an error or defined rejection.
Tests should check behavior rather than implementation details. For a number-classification , useful cases include a positive number, a negative number, and zero. Python's unittest framework provides test cases, assertions, test suites, and test runners; methods whose names begin with test are recognized by the test runner.
Testing should be repeatable. When a bug is found, first add a test that demonstrates the bug. After changing the code, confirm that the new test passes and that existing tests still pass. This guards against regression, in which a later change accidentally breaks previously working behavior.
Takeaway: Test normal behavior, boundaries, empty and invalid inputs when relevant, and repeat tests after every significant change.
Design Review and Key Principles
Several recurring design problems make programs harder to understand and maintain.
Functions that do too much: Separate input, calculation, state changes, and presentation when they represent different responsibilities.
Hidden side effects: Avoid silently changing global data, files, or mutable inputs; document necessary changes.
Unclear parameters: Use descriptive names instead of vague names such as
dataorvaluewhen the context is not obvious.Repeated code: Extract genuinely common behavior into a reusable , but avoid abstractions that group unrelated operations.
Insufficient tests: Include boundary and invalid cases instead of testing only one ordinary input.
A strong overall design combines focused functions, explicit inputs and outputs, limited global state, clear interfaces, , and repeatable tests. These practices make a program easier to trace, modify, and verify.
Takeaway: Good structure is the combined result of clear responsibilities, visible data flow, controlled side effects, and tests that represent the important cases.