Branching on Types With isinstance

Concepts covered: Guard Clauses

Data that crosses a system boundary rarely arrives in a single shape. A quantity field decoded from a partner's JSON can be 12 or "12". It can also be None, or even True. The code receiving it has to decide what each shape means, and isinstance(value, int) is how a branch asks whether a value is an int.

isinstance(value, T) returns True when value is of type T or of a type built on top of T. The second half matters for one pairing in particular: bool is built on int, which is why True + 1 is 2 and isinstance(True, int) is True. The exact check type(value) is int accepts only int itself, so it says False for a bool.

qty = True
print(isinstance(qty, int))
print(type(qty) is int)
print(isinstance(qty, bool))
print(qty + 1)
print(isinstance(3.0, int))

Output:

True
False
True
2
False

That subtype rule is the classic hole in a quantity check. A test written as isinstance(qty, int) lets True through, and a boolean flag that leaked into the quantity column gets counted as 1. Test for bool before int, and reject it.

Normalize at the boundary

A type check belongs where mixed shapes enter your code. A single function receives the raw value, sorts it by type, and hands back either a clean int or None. Everything downstream works on that one type, so no other function needs to ask.

def parse_qty(raw):
    if raw is None:
        return None
    if isinstance(raw, bool):
        return None
    if isinstance(raw, int):
        return raw
    if isinstance(raw, str):
        return int(raw)
    return None

a = parse_qty("12")
b = parse_qty(True)
c = parse_qty(7)
d = parse_qty(2.5)
total = a + c
print(a, b, c, d)
print(total)
12 None 7 None
19

Each test in parse_qty is a type question, and their order matters for the reason guard order mattered earlier in this lesson: the bool test must come before the int test, or True would pass through as a quantity. A float such as 2.5 fails every test and reaches the final return None. int("12") converts a numeric string, while int("twelve") would stop the program with a ValueError, which the lesson on handling errors covers.

When a type check is hiding a problem

The warning sign is the same isinstance ladder appearing in a second and a third function. It means values were never normalized when they arrived. Every consumer then re-decides what "12" means, and sooner or later 2 copies decide differently. The fix is structural: convert once at the edge, and let everything after it assume a single type.

A type check worth keeping
  • It sits where raw data enters the code
  • Each branch converts the value to a single target type
  • It rejects bool before testing for int
  • Downstream code never repeats it
A type check that hides a design problem
  • The same ladder is copied into several functions
  • It branches to run an operation both types already support
  • It rejects a float or int the code would handle fine
  • It stands in for fixing the producer that sends mixed types

A second warning sign is a check that adds nothing. if isinstance(x, int) or isinstance(x, float): y = x + 1 branches on type to run an operation both types already support. Without the check, numbers behave the same. If None or "12" arrives, which should never happen, it fails loudly with a TypeError right where it's used.

What does this print?

def describe(v):
    if isinstance(v, int):
        return "count"
    if isinstance(v, bool):
        return "flag"
    return "other"

print(describe(False))
print(describe(2.0))

Capstone: a reading through every tool

These last 2 drills put the whole lesson into a single function. Guards leave early and a type check sits at the boundary. After that come a chained range and an inline label.

The second capstone hinges on a habit worth keeping for any type ladder: feed it one value of every shape it could receive and note which test fires first. Give bools their own rows, since they are the values that slip into an int branch, and include 0, a real quantity that must come back as 0 and never as a missing value. Here is that check run on parse_qty from the animation.

InputFirst test that is Trueparse_qty returns
`None``raw is None``None`
`False``isinstance(raw, bool)``None`
`0``isinstance(raw, int)``0`
`"7"``isinstance(raw, str)``7`
`2.5`none of them`None`

About this lesson

This section is part of the Control Flow: Intermediate lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.