PCEP study materials
PCEP study resources

Temperature Converter with a Sticky Default List

PCEP · Put your knowledge to work in context.

All case studies

Temperature Converter with a Sticky Default List

A lab technician writes a converter for a logging tool:

def to_celsius(f, history=[]): result = (f - 32) * 5 / 9 history.append(f) return result

def safe_convert(text): try: value = int(text) print(to_celsius(value)) except ValueError: print("not a number")

The tool calls safe_convert("50"), safe_convert("50.5"), and safe_convert("abc"). The first prints 10.0. After a long session the technician notices memory growing, because history keeps entries from every call, including calls made by other modules that never asked for history. safe_convert("50.5") prints not a number even though 50.5 is a number, and a later call with None raises an uncaught TypeError instead of the friendly message. The team wants history kept only when a caller explicitly supplies a list.

1. Explain why `history` accumulates across independent calls, and give a corrected signature and body.

Read the worked answer

Default argument values are evaluated once, when the def statement executes, and the resulting object is stored on the function. history=[] therefore creates one list for the lifetime of the program, and every call that does not pass history appends to that same object. The growth is not a leak in the usual sense; it is shared mutable state.

Corrected version:

def to_celsius(f, history=None):
    if history is None:
        history = []
    result = (f - 32) * 5 / 9
    if history is not None:
        history.append(f)
    return result

With the simple fix above, history is a fresh list for each call, so nothing escapes unless the caller passes their own list and keeps a reference to it. The test must be is None, not if not history: an empty list supplied by the caller is falsy, and a truthiness check would silently replace it with a new list, discarding the caller's object. The same trap applies to {} and set() defaults.

2. Explain why `safe_convert("50.5")` and `safe_convert(None)` behave differently from `safe_convert("abc")`, and change the handler so both produce the friendly message.

Read the worked answer

int() parses integer text only. int("50.5") fails on the decimal point and raises ValueError, the same exception class as "abc", so the message is misleading: the input is a number, just not an integer.

int(None) is a different failure. None is not a string or number at all, so the argument has the wrong type and Python raises TypeError, which is not a subclass of ValueError. The except ValueError clause never matches, and the exception propagates.

Fix - parse as float when decimals are legal, and catch both classes:

def safe_convert(text):
    try:
        value = float(text)
    except (ValueError, TypeError):
        print("not a number")
        return None
    return to_celsius(value)

Catching the tuple is preferred to a bare except: or except Exception:, which would also hide genuine bugs such as a misspelled variable name inside the block.

3. A colleague writes `x = safe_convert("abc") + 1` and gets a `TypeError`. Explain the cause in terms of return values, and propose a contract that prevents it.

Read the worked answer

safe_convert prints inside the except branch but has no return there. A function that finishes without an explicit return (or falls off the end) yields None. So x is None, and None + 1 raises TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'.

This is the deeper problem: the function mixes two jobs, printing for humans and computing for programs. A better contract returns a value and lets the caller decide how to report it - return None on failure and document it, or re-raise and let the caller handle it, or return a sentinel such as float("nan"). If None is the failure signal, every caller must test for it before doing arithmetic.

A cleaner alternative is to let the exception propagate (raise inside the handler) and keep the printing in the user-interface layer, so the data path never silently produces None that only crashes three lines later.