Module 2/5 · Weeks 4–6 · 30 h

Control structures and functions

DRT 114 Computer Programming

About 90 minDraft, awaiting reviewLast updated 26 September 2026

Lesson

By the end of this module you will be able to

  1. Write if/elif/else decisions and loops with for and while
  2. Use lists, tuples and dicts to hold mission data
  3. Write functions with docstrings, default values and return values
  4. Handle errors appropriately with exceptions
  5. Test functions with assert and pytest

Prerequisites: DRT 114 module 1

Why this matters

Useful programs must decide, for example warning when the battery is low, and repeat, for example computing every leg of a route. As code grows, it must be split into functions with clear jobs that can be tested separately and reused. The autopilots that fly real aircraft are built from the same elements, only on a much larger scale.

Deciding with if

This example grades a battery against PX4’s default thresholds in the version checked (low 15%, critical 7%), covered in DRT 342 module 2.

Flowchart: input percent; check whether it lies between 0 and 100, otherwise ValueError; if percent is at most 7 return CRITICAL; otherwise if at most 15 return LOW; otherwise OK
Figure 1 Flowchart of battery_status
def battery_status(percent):
    """Return "OK", "LOW" or "CRITICAL" for a battery percentage (0-100)."""
    if not 0 <= percent <= 100:
        raise ValueError("percent must be between 0 and 100")
    if percent <= 7:
        return "CRITICAL"
    if percent <= 15:
        return "LOW"
    return "OK"


for level in [100, 16, 15, 8, 7, 0]:
    print(level, battery_status(level))
100 OK
16 OK
15 LOW
8 LOW
7 CRITICAL
0 CRITICAL

Order matters. <= 7 must be checked before <= 15; swap them and 5% returns “LOW” instead of “CRITICAL”. Values exactly on a boundary, such as 15 and 7, must always be tested.

Handling errors

Given an impossible value such as 120%, a function should not guess. It should raise an exception so the caller knows, and the caller chooses how to handle it with try/except.

readings = [45, 120, 12]
for reading in readings:
    try:
        print(reading, battery_status(reading))
    except ValueError as err:
        print(reading, "rejected:", err)
45 OK
120 rejected: percent must be between 0 and 100
12 LOW

Catching everything with a bare except: and doing nothing is a dangerous habit, because it hides the real problem. Always name the error type you expect.

Loops and data structures

A list holds an ordered, changeable sequence; a tuple holds data that should not change, such as one coordinate; a dict holds name–value pairs.

Example 1 Route length and flight time

A 200 × 100 m rectangular route in local coordinates (metres east, north), flown at 8 m/s.

import math

waypoints = [(0, 0), (200, 0), (200, 100), (0, 100), (0, 0)]
drone = {"id": "RSU-QUAD-01", "cruise_ms": 8.0}

total_m = 0.0
for start, end in zip(waypoints, waypoints[1:]):
    leg = math.dist(start, end)
    total_m += leg
    print(f"{start} -> {end}: {leg:.0f} m")

minutes, seconds = divmod(total_m / drone["cruise_ms"], 60)
print(f"total {total_m:.0f} m, {minutes:.0f} min {seconds:.0f} s")
(0, 0) -> (200, 0): 200 m
(200, 0) -> (200, 100): 100 m
(200, 100) -> (0, 100): 200 m
(0, 100) -> (0, 0): 100 m
total 600 m, 1 min 15 s

zip(waypoints, waypoints[1:]) pairs neighbouring points, a pattern used constantly with routes and time series.

Reusable functions

A good function does one job, has a name that says what it does, has a docstring describing what it takes and returns (PEP 257), and does not quietly change outside values.

Three groups of inputs, lat1 lon1, lat2 lon2, and radius with a default of 6 371 000 metres, enter the function distance_m using the haversine formula, which returns a distance in metres; a docstring and tests sit below the function
Figure 2 A function takes inputs and returns a result

Example 2 Is the site inside VTR1?

Using DRT 343 module 2: VTR1 is a circle of 10 NM radius around 13°45′54″N 100°32′18″E, and the site is at 13°50′00″N 100°40′00″E.

EARTH_RADIUS_M = 6_371_000.0
NM_TO_M = 1852


def distance_m(lat1, lon1, lat2, lon2, radius=EARTH_RADIUS_M):
    """Great-circle distance in metres between two points given in degrees."""
    phi1, phi2 = math.radians(lat1), math.radians(lat2)
    d_phi = phi2 - phi1
    d_lambda = math.radians(lon2 - lon1)
    a = math.sin(d_phi / 2) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(d_lambda / 2) ** 2
    return 2 * radius * math.asin(math.sqrt(a))


def dms_to_deg(degrees, minutes, seconds):
    """Convert degrees, minutes and seconds to decimal degrees."""
    return degrees + minutes / 60 + seconds / 3600


vtr1_centre = (dms_to_deg(13, 45, 54), dms_to_deg(100, 32, 18))
site = (dms_to_deg(13, 50, 0), dms_to_deg(100, 40, 0))

d = distance_m(*vtr1_centre, *site)
print(f"{d / 1000:.1f} km, inside VTR1: {d <= 10 * NM_TO_M}")
15.8 km, inside VTR1: True

This matches the result in DRT 343. The * before a tuple unpacks it into separate arguments, and underscores in 6_371_000.0 make the digits easier to read.

Testing functions

assert checks that a condition is true; if not, the program stops and reports it immediately. Use it with known answers, including boundary values and impossible inputs.

assert battery_status(16) == "OK"
assert battery_status(15) == "LOW"
assert battery_status(7) == "CRITICAL"
assert math.isclose(distance_m(0, 0, 0, 1), 111_194.9, abs_tol=0.1)

try:
    battery_status(-1)
except ValueError:
    print("all tests passed")
all tests passed

As a project grows, move tests into test_*.py files and run them with pytest, which requirements.txt already installs.

# test_battery.py
import pytest
from mission_tools import battery_status

def test_boundaries():
    assert battery_status(15) == "LOW"
    assert battery_status(7) == "CRITICAL"

def test_rejects_impossible_values():
    with pytest.raises(ValueError):
        battery_status(120)

Run python -m pytest in the work folder.

Module lab

Lab: a mission tools library

  1. Create mission_tools.py with battery_status(), distance_m() and route_length_m(), which takes a list of latitude–longitude points, each with a docstring.
  2. Write test_mission_tools.py covering boundary values, impossible values and known answers (1 degree of longitude on the equator is 111 194.9 m), and make pytest pass.
  3. Write a function that checks every waypoint is within a given geofence radius of the take-off point, and reports those that are not.
  4. Swap test files with another group and run them against their code. Record the cases that break the other group’s code.

Common mistakes

Watch out

  • Ordering conditions wrongly, so a narrower condition is never reached
  • Not testing boundary values, such as exactly 15
  • Using a bare except:, hiding real errors
  • Using a list as a default parameter, such as def f(points=[]), which carries values across calls; use None and create a new list inside
  • Passing degrees to trigonometric functions that expect radians

Summary

  • if/elif/else makes decisions; the order of conditions changes the answer
  • for loops over data; zip(a, a[1:]) pairs neighbouring items
  • Lists, tuples and dicts hold different kinds of mission data
  • Good functions do one job, have docstrings, and raise errors on impossible input
  • Test with assert and pytest, focusing on boundaries and known answers

Check your understanding

  1. What does battery_status(15) return, and why?
  2. If percent <= 15 were checked before percent <= 7, what would 5 return?
  3. What is list(zip([1, 2, 3], [2, 3]))?
  4. How long is the route (0, 0) → (30, 40) → (30, 100) in metres?
  5. Why should you not write def add_point(points=[])?
Answers
  1. "LOW", because 15 is not above 15 but is above 7
  2. "LOW", which is wrong, because the first condition catches it before the critical check
  3. [(1, 2), (2, 3)]
  4. m
  5. The default list is created once and shared by every call, so values from earlier calls remain

Key formulas

Great-circle distance (haversine)
Length of a multi-waypoint route

Key references

  1. Python Software Foundation. The Python tutorial (Python 3.14 documentation). link
  2. van Rossum, G., Warsaw, B., & Coghlan, A. (2001). PEP 8 – Style guide for Python code. link
  3. Goodger, D., & van Rossum, G. (2001). PEP 257 – Docstring conventions. link
  4. Downey, A. B. (2024). Think Python: How to think like a computer scientist (3rd ed.). O’Reilly. link
  5. Matthes, E. (2023). Python crash course (3rd ed.). No Starch Press.
  6. PX4 Autopilot. PX4 user and developer guide. link

Further reading

Study the assigned knowledge units in advance, review media and take the module quiz

In class / field

Lab or field practice from worksheets with a safety checklist

Learning evidence: Checked worksheets and quiz results

Module quiz

This is a formative self-check, not a graded exam

Knowledge domain: Programming and digital technology