PYTHON • PROBLEM-SOLVING PATTERNS

Common Python Edge Cases — Avoid common Python edge cases (integer division, mutable defaults, None handling)

Master the sneaky Python pitfalls that trip up even experienced programmers and learn defensive coding patterns.

Historical Context & Motivation

Every programming language carries a set of design decisions that can surprise newcomers. Python, despite its reputation for readability and simplicity, contains several edge cases — unexpected behaviors that arise in specific situations. These quirks are not bugs; they are consequences of deliberate choices made by Python's creator, Guido van Rossum, and the core development team over decades of evolution. Understanding these edge cases is essential because they cause real bugs in real programs, from homework assignments to professional software.

Many of these pitfalls stem from Python's transition from version 2 to version 3, its approach to memory management with mutable objects, and its use of None as a special sentinel value. Let's trace the key moments that shaped these behaviors.

1991
Python 1.0 Released
Guido van Rossum releases Python with C-style integer division, where dividing two integers always produces an integer — a behavior inherited from languages like C and Java.
2000
Python 2.0 & Mutable Defaults
Python 2.0 introduces list comprehensions and other features. The mutable default argument behavior — where default lists and dicts are shared across calls — is already present and starts generating widespread confusion in online forums.
2001
PEP 238 Proposes True Division
Recognizing that integer division causes countless bugs, PEP 238 proposes changing the / operator to return a float. The // operator is introduced for explicit floor division.
2008
Python 3.0 Fixes Division
Python 3 makes true division the default: 7 / 2 now returns 3.5. However, the mutable default and None handling behaviors remain, continuing to challenge programmers.
2020s
Modern Best Practices
Type hints, linters like pylint and mypy, and style guides now explicitly warn against mutable defaults and improper None checks. Defensive coding becomes standard practice.

The central question this lesson addresses is: What are the most common Python edge cases, and how can you write code that avoids them? By the end of this lesson, you'll recognize these traps before they bite you and know the defensive patterns that professional developers use daily.

Core Principles & Definitions

Before diving into specific edge cases, you need to understand three foundational concepts that explain why these pitfalls exist. Each one connects to how Python manages data behind the scenes.

1

Integer Division vs. True Division

The / operator performs true division (returns a float), while // performs floor division (rounds down to the nearest integer). Mixing these up — especially with negative numbers — creates subtle bugs.
2

Mutable Default Arguments

When you use a mutable object like a list [] or dictionary {} as a default parameter in a function, Python creates that object once at definition time and reuses it across every call.
3

None Handling & Identity Checks

None is Python's null value — a singleton object meaning 'no value.' Checking for None requires is None (identity check), not == None (equality check), because equality can be overridden.
4

Mutable vs. Immutable Objects

Immutable objects (int, str, tuple) cannot be changed after creation. Mutable objects (list, dict, set) can be modified in place. This distinction is the root cause of the mutable default problem.
✦ KEY TAKEAWAY
Think of a mutable default argument like a shared whiteboard in a classroom. The first student writes on it, and when the next student comes in, the board still has the first student's writing. Each function call sees the same whiteboard — the same list object — not a fresh one. The fix is to use None as the default and create a new object inside the function each time.

Visual Explanation — How Mutable Defaults Surprise You

The diagram below illustrates what happens when you define a function with a mutable default list and call it multiple times. On the left, you see the broken pattern where the default list accumulates items across calls. On the right, you see the correct pattern using None as the sentinel.

Left: the broken pattern where a default list [] accumulates data across function calls. Right: the correct pattern using None as a sentinel and creating a fresh list inside the function body.

Notice how on the left side, the list keeps growing with each call. This is because Python evaluates default arguments exactly once — when the def statement runs. The list object is then stored as part of the function's metadata and reused. On the right side, the None default is immutable, so a brand-new list is created on each call inside the function body.

How Each Edge Case Works Under the Hood

Edge Case 1: Integer Division & Floor Division

In Python 3, the / operator always returns a float, even when both operands are integers. The // operator performs floor division, which rounds the result toward negative infinity — not toward zero. This distinction matters especially with negative numbers.

TRUE DIVISION
7 / 2 → 3.5
The / operator always produces a float result, even for integer operands.
FLOOR DIVISION (POSITIVE)
7 // 2 → 3
Floor division rounds down. For positive numbers, this behaves like truncation.
FLOOR DIVISION (NEGATIVE) — THE TRAP
−7 // 2 → −4 (not −3!)
Floor division rounds toward negative infinity. Since −3.5 rounds down to −4, you get −4 instead of the −3 you might expect from simple truncation.

Edge Case 2: Mutable Default Arguments

When Python encounters a def statement, it evaluates the default argument expressions immediately and stores the resulting objects. For immutable defaults like integers or strings, this is fine because they cannot change. But for mutable defaults like lists, dictionaries, or sets, the same object is shared across every call that does not explicitly provide that argument. Any modifications accumulate in the shared object.

Edge Case 3: None Handling

None is a singleton in Python — there is exactly one None object in memory. You should always check for it using is None or is not None rather than == None. The is operator checks identity (same object in memory), while == checks equality (which a class can override via the __eq__ method). A poorly written class could make x == None return True even when x is not actually None.

⚠️ Watch Out: Falsy Values
In Python, several values are falsy — they evaluate to False in boolean context. These include None, 0, "" (empty string), [] (empty list), and {} (empty dict). Writing if not x: when you mean if x is None: can accidentally catch 0 or empty strings, leading to incorrect behavior.

Detailed Breakdown — Division Behavior & Falsy Traps

The following diagram maps out the full decision tree for division operations in Python 3 and highlights the negative-number trap that catches many students. Below the diagram is a comprehensive table of falsy values and the correct way to check for each one.

A decision tree showing how Python 3 handles the / and // operators. The red box highlights the negative floor division trap: −7 // 2 yields −4, not −3.
Python falsy values and the correct way to check for each
ValueTruthinessCheck for None?Correct Check
NoneFalsyYesx is None
0FalsyNo — it is a valid intx == 0
""FalsyNo — it is a valid strx == ""
[]FalsyNo — it is a valid listlen(x) == 0
FalseFalsyNo — it is a valid boolx is False

Worked Example — Fixing a Buggy Function

Suppose you're writing a grade tracker that stores student scores. The following buggy function uses all three edge cases we've studied. Let's identify and fix each bug step by step.

Debugging a Grade Tracker Function
1
Step 1 — Read the Buggy CodeHere is the original function: def add_grade(name, score, grades=[]): avg = score // 1 # intended to keep integer part if not name: # intended to check if name is None name = 'Unknown' grades.append((name, avg)) return grades This function has three bugs related to our edge cases.
2
Step 2 — Identify Bug #1: Mutable DefaultThe parameter grades=[] creates a single list that persists across calls. If we call add_grade('Alice', 92) and then add_grade('Bob', 85), Bob's result will also contain Alice's grade.
Fix: Change to grades=None and add if grades is None: grades = [] inside the function.
3
Step 3 — Identify Bug #2: Floor Division with NegativesThe line avg = score // 1 works fine for positive scores like 92.7 (giving 92). But if a score is negative (say, a penalty of −3.5), -3.5 // 1 gives -4.0 instead of -3 because floor division rounds toward negative infinity.
Fix: Use avg = int(score) which truncates toward zero, giving −3 for −3.5.
4
Step 4 — Identify Bug #3: Incorrect None CheckThe condition if not name: catches more than just None. It also catches the empty string "". If a student has a blank name from a form field, that might be a different case than receiving no name at all. Using if not name: conflates these two situations.
Fix: Use if name is None: to specifically check for None, or handle empty strings separately.
5
Step 5 — Write the Fixed FunctionHere is the corrected version: def add_grade(name, score, grades=None): if grades is None: grades = [] avg = int(score) if name is None: name = 'Unknown' grades.append((name, avg)) return grades
All three edge cases are now handled correctly. The function creates a fresh list per call, truncates toward zero, and checks identity for None.

Comparing Correct vs. Incorrect Patterns

The table below summarizes the dangerous patterns alongside their safe alternatives. Use this as a quick reference sheet when writing or reviewing Python code.

Quick reference: dangerous patterns vs. safe alternatives
Edge Case❌ Dangerous Pattern✅ Safe Pattern
Mutable defaultdef f(x, lst=[]):def f(x, lst=None): then if lst is None: lst = []
Negative floor div-7 // 2 → -4int(-7 / 2) → -3 (truncation toward zero)
None checkif x == None: or if not x:if x is None:
Check for empty listif x is None: (confuses None with [])if not x: when you know x is always a list, or if len(x) == 0:
Integer vs. float divisionAssuming 7/2 gives 3Know that 7/2 gives 3.5; use // for integer result
✦ KEY TAKEAWAY
Think of these edge cases like road signs. You don't need to memorize every stretch of highway, but you do need to recognize the warning signs: =[] in a function signature, // with negative numbers, and == None instead of is None. Once you learn to spot these signs, you can avoid the potholes entirely.

Connection to Advanced Concepts

The edge cases covered in this lesson are entry points into deeper Python concepts. As you advance, you'll encounter more sophisticated versions of these same patterns. Understanding the basics now sets you up for success with advanced tools like type checking, testing frameworks, and functional programming paradigms.

How this lesson's edge cases connect to advanced Python topics
This LessonAdvanced Concept
Mutable default argumentsObject identity & aliasing — understanding how Python variables are references to objects, not boxes holding values. This extends to deep copy vs. shallow copy.
None sentinel patternOptional types & type hints — using Optional[List[int]] in type annotations and static analysis with mypy to catch None errors before runtime.
Floor division behaviorModular arithmetic & the divmod() function — Python's floor division pairs with the % modulo operator, and understanding both is crucial for algorithms involving remainders, hash functions, and cyclic patterns.
Falsy value awarenessTruthiness protocols & __bool__ — custom classes can define their own truthiness by implementing the __bool__ or __len__ dunder methods.

As you continue learning Python, consider exploring type hints (introduced in Python 3.5) which let you annotate function parameters with their expected types. Tools like mypy can then catch many of these edge cases at analysis time, before your code even runs. This is a powerful step toward writing more reliable programs.

Practice Problems

PROBLEM 1 — CONCEPTUAL
Explain in your own words why using def collect(item, bag=[]): as a function signature can lead to unexpected behavior. What would happen if you called collect('apple') three times without providing the bag argument?
PROBLEM 2 — BASIC CALCULATION
Predict the output of each expression: a) 10 / 3 b) 10 // 3 c) -10 // 3 d) int(-10 / 3)
PROBLEM 3 — INTERMEDIATE
Consider this code: def process(value): if not value: return 'Nothing provided' return f'Got: {value}' What does process(0) return? What does process('') return? What does process(None) return? Explain why the function may not behave as intended and suggest a fix.
PROBLEM 4 — APPLIED
You're building a temperature logger. The function below records readings, but users report that readings from yesterday are showing up in today's log. Find and fix all bugs: def log_temp(temp_f, readings=[]): temp_c = (temp_f - 32) * 5 // 9 if temp_c == None: return readings readings.append(temp_c) return readings
PROBLEM 5 — CRITICAL THINKING
Python chose to make floor division round toward negative infinity (not toward zero as in C/Java). Research or reason about: Why might this choice be useful? Hint: think about the relationship between // and % (modulo). In Python, the equation a == (a // b) * b + (a % b) always holds true. How does rounding toward negative infinity make the modulo operator behave more consistently?

Lesson Summary

This lesson covered three critical Python edge cases that every programmer should know. Integer division in Python 3 uses / for true division (always returns a float) and // for floor division, which rounds toward negative infinity — a trap with negative numbers. Mutable default arguments like def f(x, lst=[]): create a shared object across calls; the fix is the None sentinel pattern: set the default to None and create a fresh object inside the function.

None handling requires using is None (identity check) rather than == None (equality check), because equality can be overridden. Be cautious with if not x: — it catches all falsy values (0, empty string, empty list, None), not just None. Memorize the defensive patterns: use None defaults, is None checks, and int(a/b) for truncation toward zero. These small habits prevent big bugs.

Varsity Tutors • Python • Common Python Edge Cases