Scope, lifetime and debugging in an IDE
Local and global variables, why locals are good practice, and breakpoints, stepping, watches and tracebacks.
Do this lesson in the simulatorAt GCSE you met local and global variables (F4.3) and the tools of an IDE (F6.6). At A level you need two ideas for every variable, its scope and its lifetime, the reasons local variables are good practice, and how the debugging tools of an IDE find the errors that scope mistakes cause.
Scope and lifetime
- The scope of a variable is the part of the program where its name can be used.
- The lifetime of a variable is the time during the run when it exists and holds a value.
A local variable is declared inside a subroutine. Its scope is that subroutine only, and its lifetime is one call: it is created when the subroutine is called and destroyed when it returns. Call the subroutine again and it starts afresh.
A global variable is declared in the main program, outside every subroutine. Its scope is the whole program, and its lifetime is the whole run.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
SPEED = 50 # global: every subroutine can read it
def creep(steps):
moved = 0 # local: made fresh on every call
for i in range(steps):
forward(SPEED, distance=5)
moved = moved + 5
return moved
print(creep(2))
print(creep(3))
print(moved)
The two calls print 10 and 15: moved starts at 0 each time, because each call has its own. The last line fails with NameError, because moved stopped existing when creep returned and was never in scope in the main program.
Shadowing, and changing a global
If a subroutine assigns to a name, Python makes it local, even when a global has the same name. The local one shadows the global inside the subroutine and the global is untouched. That rule causes a confusing error:
beeps = 0
def beep():
beeps = beeps + 1 # assigning makes beeps local, so it has no value to add to yet
print("beep", beeps)
try:
beep()
except UnboundLocalError as error:
print(type(error).__name__, ":", error)
The global keyword tells Python a name inside a subroutine means the global variable, and OCR's exam reference language has the same keyword: global beeps = 0. It works, but it is usually the wrong fix.
Why local variables are good practice
- Independence: a subroutine that uses only its parameters and local variables can be understood, tested and reused on its own, in another program, without the globals it would otherwise need.
- No hidden side effects: a global can be changed from anywhere, so a wrong value could have come from any subroutine. With locals, the only way in is a parameter and the only way out is a return value.
- Names can be reused: two programmers can both use
countin their own subroutines without clashing. - Memory is freed: a local exists only while its subroutine runs.
Globals are reasonable for constants that the whole program reads, like SPEED above, and for a small amount of state that genuinely belongs to the whole program.
Debugging tools
An IDE, integrated development environment, collects the tools for writing, running and debugging code. A level questions expect you to explain how each debugging tool helps:
| Tool | What it does | How it helps |
|---|---|---|
| Breakpoint | Pauses the run when it reaches a chosen line | Run at full speed to the suspect part, then look closely |
| Stepping | Runs one line at a time; step into a call, over it, or out of the subroutine | Follow the exact path the program takes |
| Variable watch | Shows the current values of variables while paused | See the moment a value goes wrong |
| Call stack view | Lists the subroutine calls that led to the current line | See which caller passed a bad argument |
| Error diagnostics | Underlines syntax errors as you type; reports a runtime error's type and line | Find the line and the kind of error at once |
| Traceback | The chain of calls printed when an exception is not handled | Read upwards from the line that failed to the call that caused it |
| Syntax highlighting, auto-completion, auto-indent | Colour, suggestions and layout in the editor | Fewer typing mistakes, and they show up sooner |
In the cells on this site, Debug steps through a program one line at a time and shows the variables beside the controls. Professional IDEs such as VS Code, PyCharm and Thonny add breakpoints set by clicking beside a line number, and a call stack panel.
When there is no debugger, as on a robot's own processor, watch lines do the same job: print the values you would have watched, each time round the loop. Press Debug on this cell and step through it, then compare what the watch line prints:
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def average(readings):
total = 0
for cm in readings:
total = total + cm
print("watch: cm =", cm, "total =", total)
return total / len(readings)
print(average([31.0, 51.0, 15.0]))
Task: fix the scope bug
This program should creep towards the wall in 5 cm steps while distance() is more than 25, then print steps: <n>, the number of steps taken. It stops with an UnboundLocalError. Run it and read the message first.
Fix it without global: give step a parameter, count (the number of steps so far, an integer), and make it return count + 1, with the main program assigning the result back. Add a watch line: before each move, step prints step <n>: <cm> cm, where <n> is the number of the step about to be taken (1 for the first) and <cm> is the value distance() returns.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
steps = 0
def step():
forward(50, distance=5)
steps = steps + 1
while distance() > 25:
step()
print("steps:", steps)
Challenges
- Fix the program the other way, with
global steps. Then list what a reader ofstepnow has to check that they did not before. - Make
creepin the first cell take the speed as a parameter instead of reading the globalSPEED. What does that make easier to test? - Put
print(beeps)just beforebeeps = beeps + 1in the second cell. Why does that line fail too, when reading a global normally works?