Did you know that a Python function can print the correct answer and still return None? Printing a value is not the same thing as returning it to the caller. That distinction can make a program appear successful on a screen while failing the next calculation. Functions become easier to understand when students trace arguments, parameters, return values and the scope of variables separately.
Ministry of Education V3.0 Reference 369 explains the 2027 Singapore–Cambridge SEC G3 Computing K349 outcomes 2.3.13 and 2.3.14: defining and calling user-defined Python functions, passing arguments, receiving return values, and distinguishing local variables from global ones. It provides short runnable examples, systematic traces, common-error diagnostics and original classroom scenarios. The focus is on learning to explain what values exist at each step and how a function connects to the rest of a program.
This independent eduKateSG resource is not an official MOE, SEAB or Cambridge syllabus. The authoritative K349 document and Python quick-reference guide determine examinable boundaries. All records, programmes and scenarios in this reference are invented. Students should create and debug their own assessed solutions under their school’s rules, using synthetic rather than sensitive personal information in practice.
1. The official scope: user-defined functions and variable visibility
The 2027 K349 syllabus states that students should write and call user-defined functions that may accept parameters and/or provide a return value. It separately requires an understanding of the purpose and use of local and global variables in programs that use functions. The quick-reference guide shows a def statement, a parameter list and a return statement.
A candidate need not build an advanced object-oriented architecture to demonstrate these outcomes. The educational focus is on a clear function interface: what information enters, what computation happens and what comes back. Closely related programming and testing outcomes are useful context but should not obscure these explicit requirements.
2. def creates a function; a call executes it
Writing a function definition tells Python the name and the statements that form the function body. Those statements do not necessarily execute when the interpreter first reads the definition. The function body runs when code calls the function with a suitable argument list.
For example, defining a function to double a number does not immediately double every number in the program. Calling double(7) executes the specified operation with input seven. Students should distinguish the time a function is defined from the times it is called, because tracing execution order is a common examination task.
3. Parameters are names in the definition
In def double(n):, the name n is a parameter. It represents an input that the function can receive when called. When the program calls double(7), the value 7 is the argument supplied at that call. The parameter name is used within the function body.
An argument may also be an expression or variable rather than a literal number. If score = 5 and the caller uses double(score), the value available through the parameter is five. This does not imply that the name score becomes a local variable inside the function.
4. Return sends a value back to the caller
The statement return n*2 causes the function to provide a computed result to its caller. A call double(7) can therefore be used in a larger expression, such as double(7) + 1, producing fifteen. The caller can store that returned result in a new variable.
A return statement also ends the current function execution. Statements after an unconditional return in the same execution path will not run. This is important for trace problems involving multiple return statements under different conditions. Ask which path is taken before deciding the returned value.
5. print displays output but does not replace return
A function that calls print(n*2) displays a value but normally returns None if it reaches the end without an explicit return. An assignment result = show_double(7) can therefore give result the value None even though fourteen appeared on the screen.
This is a frequent source of confusion. A learner who sees fourteen printed may mistakenly believe result contains fourteen and attempt arithmetic with it, causing a type error. Trace the printed side effect and the actual returned value separately. A correct console display is not proof that the function’s interface meets the requirement.
6. Functions can be called more than once
A function definition can be reused with different arguments. double(3) returns six, while double(10) returns twenty. The operations follow the same code but begin with different input values. This is one reason functions help reduce duplicated instructions.
Each call has its own local parameter bindings and execution. A variable value in one call should not be assumed to carry into the next call unless the programme deliberately uses some persistent state. In a beginner trace, treat each call as an evaluation of the function with the arguments supplied at that point.
7. More than one parameter is possible
A function def add(a,b): return a+b accepts two inputs. The call add(3,4) returns seven. The order of arguments matters in general; for subtraction, subtract(10,3) and subtract(3,10) return different results.
Students should verify the number of arguments as well as their positions. A call with too few or too many positional arguments raises an error when the function signature does not permit it. The design of the parameter list should match the problem’s data requirements, not merely the programmer’s preferred variable names.
8. A local variable belongs to a function’s scope
A variable assigned inside a function is typically local to that function unless scope is explicitly managed differently. Another function or the surrounding top-level code cannot automatically read that local name. Local variables help separate the temporary work of one calculation from unrelated values elsewhere.
For instance, if a function sets temporary = 9 and returns temporary*2, the caller receives eighteen but does not gain a top-level variable named temporary merely because the function used one. The result crosses the interface through return, not through accidental exposure of local working names.
9. A global variable exists outside a function
A name assigned in the module’s top-level scope is global to that module. A function can often read its value if no local binding shadows it. But relying on hidden external state can make functions harder to test and reuse, because the result may depend on something not visible in the argument list.
For introductory examples, passing a needed value as an argument is usually clearer. The syllabus asks students to distinguish the purpose of local and global variables, not to treat global variables as a shortcut for every calculation. Good explanations identify where the name is created and where it can be accessed.
10. A local name can shadow a global name
Suppose a global variable x is 10, and inside a function a local parameter also has the name x. The parameter’s local binding is used inside the function. Calling that function with x=3 may return a result based on three while leaving the global value ten unchanged.
This is not a contradiction: two different scopes contain names with the same spelling. The program chooses the appropriate binding according to scope rules. To make the trace legible, label the names explicitly as global x and local x rather than pretending the entire program has one universal variable named x.
11. Assignment inside a function is not automatically global
If code assigns x = 5 inside a function, Python normally treats x as a local name in that function. A programmer who wants the caller to use the computed value should return it rather than assume that the top-level x has changed.
More advanced code can use a global declaration, but that changes the data-flow model and may introduce hard-to-track side effects. Within the syllabus’s foundational distinction, students should understand the normal local behaviour and the benefit of explicit inputs and returned outputs. Do not invent persistent effects that the code does not create.
12. A name can be unbound before assignment
If a function assigns to x anywhere in its body, Python normally treats x as local throughout that function unless declared otherwise. Attempting to read that local x before the assignment has executed can raise UnboundLocalError, even when a global variable of the same name exists.
The key lesson is to analyse the scope established by the program, not simply search for a matching name elsewhere on the page. A trace should identify whether a variable is local, global or a parameter and whether its value exists at the point it is used.
13. Returning a calculation improves testability
A function such as add_tax(amount,rate) that returns a computed value can be checked independently with known test inputs. A function that prints its result but provides no return value is harder to reuse in later computations. Neither printing nor returning is inherently forbidden; they serve different purposes.
A good design question asks what the caller needs. If the purpose is displaying a final message, print may be appropriate at an outer layer. If another function or statement needs the numerical result, return is the natural interface. This separation improves clarity and allows normal, boundary and error cases to be tested.
14. Functions support a modular solution
A large programme can be decomposed into smaller functions, each responsible for a defined task. For example, one function validates an input range, another computes a fictional total and another constructs an output message. Each part can be understood and tested against a narrower requirement.
Decomposition is useful only when the boundaries make sense. Splitting one simple arithmetic line across many tiny functions can make a program harder to read. The educational goal is clear responsibility, explicit data movement and correct, testable behaviour rather than the greatest possible number of definitions.
15. Trace expressions in evaluation order
If x = double(3) and y = double(x), then x becomes six and y becomes twelve. The second call uses the result of the first. To predict the final values, write the arguments for each call and the returned value before proceeding to the next statement.
This method also catches an important failure: if double printed but did not return, x would be None, so a later numerical calculation could fail. Record output and return separately. An expression may have both a visible print and an invisible returned value.
16. Scope is a tool for reducing unintended effects
A local working variable can be changed without altering an unrelated global name. That isolation makes a function easier to reason about and easier to test. A global variable may be appropriate for shared module-level information, but it should be used deliberately and understood clearly.
In teaching, learners often improve when the tutor asks: ‘Where was this variable created? Which execution path assigns it? Does the result leave through return?’ Those questions replace vague speculation with inspectable facts about function calls and scopes.
def double(n):
return n * 2
def show_double(n):
print(n * 2)
x = double(7) # 14
printed_result = show_double(7) # prints 14
print(printed_result) # None
base = 10
def compare(base):
local_result = base + 1
return local_result
print(compare(3)) # 4
print(base) # 10
Twelve-stage Call–Scope–Return analysis
This original method is an independent study routine, not an official mark scheme. It helps learners predict behaviour even when definitions, multiple calls, local variables and print statements appear together. Record both the visible output and actual returned value.
Stage 1 — Locate each definition
Identify the function name, parameters and statements inside its indented body. A definition is not automatically a call.
Stage 2 — Locate the actual calls
Follow the order in which the programme calls functions, not the order in which names are first mentioned.
Stage 3 — Match arguments to parameters
Assign supplied argument values to the local parameter names for this specific call.
Stage 4 — Mark local bindings
Identify parameters and variables created inside the function. These names normally belong to the local scope.
Stage 5 — Mark relevant global values
Record module-level variables and distinguish them from local names with the same spelling.
Stage 6 — Evaluate selection or loop paths
Determine which conditions and iterations occur before predicting the final value returned.
Stage 7 — Record each intermediate state
Trace changes to local working variables in order, especially assignments that happen before return.
Stage 8 — Distinguish print from return
Note any displayed output separately. A function can print a number yet return None.
Stage 9 — Stop when return executes
An executed return ends the current call. Later statements in that path are not executed.
Stage 10 — Transfer returned values
Substitute the returned value into the caller’s expression, assignment or subsequent function call.
Stage 11 — Check what remains in scope
Do not assume that a local working variable becomes accessible in the caller merely because the function ended.
Stage 12 — Verify through a contrasting call
Change the argument or global context and predict whether the behaviour changes for a principled reason.
Twelve worked function traces
Worked case 1 — Definition without a call
A programme defines def f(): print(‘Hi’) but never calls f. Nothing from that function body is printed. Defining a function creates the callable object; it does not execute its body.
Worked case 2 — One argument
With def double(n): return n*2, the call double(7) returns 14. The name n is the local parameter, while seven is the argument supplied by the caller.
Worked case 3 — Two calls
The calls double(3) and double(10) return 6 and 20. Each call uses the supplied argument independently; no value from one invocation becomes the next call’s parameter automatically.
Worked case 4 — Two parameters
For def add(a,b): return a+b, the call add(3,4) yields 7. The parameter names are local to the function and receive the arguments in the specified positions.
Worked case 5 — Argument order matters
For def subtract(a,b): return a-b, subtract(10,3) gives 7 but subtract(3,10) gives -7. A student cannot reorder arguments merely because addition happens to be commutative.
Worked case 6 — Printing is not returning
A function show(n) whose only statement is print(n*2) displays the doubled value, but a call to show returns None. Assigning its result does not give the displayed number.
Worked case 7 — Return used in an expression
If double(4) returns 8, the expression double(4)+3 is 11. The caller receives the returned integer and can use it in another calculation.
Worked case 8 — A local working value
A function computes temp=9 and returns temp*2. The caller receives 18, but the local name temp does not become a new module-level variable automatically.
Worked case 9 — Local shadows global
Suppose global x=10 and a function f(x) returns x+1. Calling f(3) returns 4, while the global x remains 10. The two x names belong to different scopes.
Worked case 10 — Reading a global value
If base=10 at module level and def f(): return base+2, then f() returns 12 when called while base still equals 10. The function reads the accessible global name without assigning it.
Worked case 11 — Return ends the call
For def f(): return 4; print(‘later’), the print after the unconditional return is not executed. The function returns four and stops on that path.
Worked case 12 — A missing returned result
If def f(n): print(n+1) has no explicit return, then z=f(3) displays 4 but z is None. A later attempt to add one to z as a number can fail.
Twelve additional scope and interface cases
Worked case 13 — Same name in different scopes
A global variable count=5 and a function-local count=2 can coexist. Returning the local count yields 2 without replacing the global count of 5. Names must be tracked with their scope.
Worked case 14 — Assignment creates a local
A function with an assignment x=5 normally has a local x, even if global x=10 also exists. Calling the function does not automatically overwrite the module-level x.
Worked case 15 — Reading before a local assignment
If a function refers to x before its own x=x+1 assignment, Python treats x as local and an UnboundLocalError can occur. A matching global x does not silently rescue that read.
Worked case 16 — Return in an if branch
If a function returns ‘high’ when n>=10 and otherwise returns ‘low’, then a call with n=10 follows the inclusive first branch. The actual return depends on the condition that executed.
Worked case 17 — A function can return zero
Returning 0 is not the same as returning None. A caller can perform normal arithmetic with integer zero, while a missing numerical return may cause a later type error.
Worked case 18 — Nested calls
If double(3) returns 6 and double(double(3)) is evaluated, the outer function receives six and returns 12. Trace the inner call completely before beginning the outer one.
Worked case 19 — Parameter reassignment
Inside def f(n): n=n+1; return n, the returned value for f(4) is 5. The local parameter name changes within that call; it does not require changing a module-level variable of another name.
Worked case 20 — Printing inside a returned function
A function can print ‘checking’ and then return 9. A caller sees the printed message and may separately store integer 9. These are two distinct effects.
Worked case 21 — A missing argument
Calling a function defined with two required parameters but supplying only one normally raises TypeError. The function interface must be satisfied before the body can perform its intended calculation.
Worked case 22 — Local name unavailable outside
If a function assigns temp=12 locally and returns 12, another top-level expression print(temp) need not be valid. The caller receives the value, not the local variable name.
Worked case 23 — An independent second call
A function using a local total initially set to zero can compute a new total each time it is called. One call’s local total is not automatically preserved for the next invocation.
Worked case 24 — Returning the wrong type
A function meant to return a number instead returns the string ’12’. A later attempt to add an integer may fail or behave differently. The return value must meet the specified type contract, not merely look numerical.
Connected studio problems for independent thinking
A fictional library-return calculation
Imagine a function days_overdue(returned,due) that returns the positive number of days late or zero if the book is returned on time. The two dates or day numbers are arguments, and the resulting count is returned for later use. The function should not secretly obtain a particular student’s information from an unrelated global variable.
For a synthetic practice case with returned day 15 and due day 12, the function may return 3 under a simple day-number model. For a return on day 10, it should return zero under the stated rule. A tutor can ask how the boundary at the due date affects the conditional branch and whether the return type remains numerical in every path.
Why a printed answer can be misleading
A student writes a function that calls print(total) and sees the expected count on screen. Later the program assigns result = function() and tries to multiply result by two. If the function reached its end without returning a number, result is None. The programme has an interface defect even though the display seemed correct.
A clear solution separates the computational function from the display decision. The function returns the count, the caller receives it and the surrounding program prints or saves it as required. This also makes the function easier to test because the result can be compared directly with an expected value.
Scope limits help prevent accidental changes
Local variables provide a contained workspace. A function can use a temporary variable named total while an unrelated global variable also happens to be called total. That does not make the two bindings identical. Deliberately passing and returning values reduces the chance that one calculation unexpectedly affects another.
This is a practical design principle rather than a demand to never use global variables. Some shared module-level settings are reasonable, but they should be understood. In an examination trace, students must follow actual Python scoping rules even when they would prefer to redesign the code. Read what the program does before judging whether it was well designed.
Trace-table layout for a nested calculation
A useful paper trace has columns for the function being called, its arguments, local parameter values, local temporary variables, printed output and returned result. For a call square(double(3)), first evaluate double(3) to obtain 6. Then evaluate square(6) to obtain 36.
Students should not combine the two calls into one unexplained leap. Explicit intermediate values reveal which function caused an error and whether the caller received the expected type. The method generalises to a sequence of multiple calls and functions with more than one parameter, without requiring complicated debugging software.
A function with an untested branch
Suppose a function returns a category only when n is positive but has no return statement for zero or negative inputs. In those other cases, normal completion returns None. A typical positive test might pass while a boundary test reveals the missing path.
Designing tests from the input requirements is therefore as important as checking one familiar result. A student should ask which conditions make each branch run, then verify that all permitted inputs produce an appropriate returned value. If an input is invalid, its required handling should be defined instead of invented after a crash.
A clean run exposes hidden state
A notebook may contain a global variable or imported function left over from an earlier execution. Code that works in the current session can fail when the notebook is restarted and run from the top. This is a particular concern when students test functions in different cells.
Use a clean, known environment when practising the lab examination. Define needed functions and variables before calls, avoid hidden dependencies, and verify any saved output required by the task. The student should be able to explain why a function works from the submitted code rather than relying on remembered execution order.
How to give feedback without taking authorship
A tutor can supply synthetic cases and ask which value is passed, which names are local, what prints and what returns. If a student’s answer is wrong, request a call-by-call trace. This develops independence rather than replacing the learner’s assessed solution.
Parents can help by describing functions as small machines with labelled inputs and outputs, but should preserve the fact that printing is not the same as returning an object. The strongest progress sign is when a student can explain that distinction in unfamiliar code and identify a local variable that should not be assumed global.
A bridge to broader software engineering
The quality of a function interface shapes testing and reuse. A function with explicit parameters and a clear returned value is usually easier to isolate than one that depends on many changing global names. Dividing a solution into sensible functions also supports the syllabus’s modular approach to problem solving.
This does not mean every task needs a complex architecture. In a small school exercise, one clear function may be enough. The purpose is to connect the code to the requirement, identify relevant states and verify correct behaviour. A reusable concept matters more than the number of definitions or lines written.
Frequently asked questions
Is a definition the same as calling a function?
No. A definition establishes the function; a call executes its body using the arguments supplied at that moment.
What is a parameter?
A parameter is a local name in the function definition that receives an argument’s value for a call.
What is a return value?
It is the value sent back to the caller by the function, which may then be assigned or used in another expression.
Does print return the number it displays?
Normally not. The print function displays information and returns None, so printing in a user-defined function is not a substitute for returning a required result.
Can a function have no parameters?
Yes. The syllabus permits user-defined functions with or without parameters and with or without explicit return values.
Does a local variable become global after the call?
No. Local variables do not automatically become available in the module scope; a return value can communicate the needed result.
Can two scopes use the same variable name?
Yes. A local name can shadow a global name without being the same binding.
Does every branch necessarily return a value?
No. If execution reaches the end without an explicit return, the call normally returns None.
How should I trace nested calls?
Evaluate the inner call, record its returned value and use that value as the argument for the outer call.
Why avoid unnecessary global state?
Explicit arguments and returns often make a function easier to understand, reuse and test, with fewer hidden dependencies.
Final independent retrieval practice
Suppose a global variable x equals 10 and a function has a parameter also named x. The call f(3) may work with the local value three while the global name remains ten. Explain why those bindings differ. Then consider a function that prints fourteen but contains no return statement: a variable assigned from the call receives None. These two short cases check the key ideas of local scope and output versus return.
Next define, without writing an assessed solution for another student, a small fictional function that accepts two numeric arguments and returns their sum. Predict its result for two different calls, then identify which names are parameters, which values are arguments and how the result reaches the caller. A tutor can vary the input and ask students to trace it aloud. This is more reliable than memorising one screenshot of code.
Official source and related eduKateSG references
Read the 2027 G3 Computing K349 syllabus, sections 2.3.13–2.3.14, and the Python quick-reference guide. Further reading: Reference 325 — Functions and Testing, Reference 361 — Software Engineering, the MOE V3.0 library and the immutable Clementi teaching reference.
Editorial review: 29 October 2026, Singapore. Every variable and dataset is fictional. The reusable habit is to trace the function call, identify local bindings, distinguish output from return, and check what reaches the caller.
