Skip to main content

Python None and Implicit Return Practice Problems & Exercises

Practice: None and Implicit Return

11 problems4 Easy4 Medium3 Hard40-55 min
← Back to lesson

Easy

#1Prove None Is a SingletonEasy
Nonesingletonidentity

Create three variables all assigned to None. Return a tuple proving they are the same object (identity check) and that their type is NoneType.

Python
def prove_singleton():
    a = None
    b = None
    c = None
    return (a is b, b is c, type(a).__name__ == 'NoneType')

print(prove_singleton())
Solution
def prove_singleton():
a = None
b = None
c = None
return (a is b, b is c, type(a).__name__ == 'NoneType')

None is a singleton — there is exactly one None object in the entire interpreter. Every variable assigned None holds a reference to the same object at the same memory address. type(None) returns <class 'NoneType'>, and you cannot create additional instances of NoneType.

def prove_singleton():
    """Create three variables assigned to None.
    Return a tuple of three booleans:
    (a is b, b is c, type(a).__name__ == 'NoneType')
    All three should be True.
    """
    # TODO: implement
    pass
Expected Output
(True, True, True)
Hints

Hint 1: Assign None to three separate variables: a, b, c.

Hint 2: Use the `is` operator to check identity — all None references point to the same object.

#2Spot the Missing ReturnEasy
implicit-returnNonebug

The function double_all builds a doubled list but never returns it. Fix the bug so the function returns the correct result instead of None.

Python
def double_all(numbers):
    result = []
    for n in numbers:
        result.append(n * 2)
    return result

def test():
    output = double_all([1, 2, 3])
    assert output is not None, "Function returned None!"
    assert output == [2, 4, 6]
    return "All tests passed"

print(test())
Solution
def double_all(numbers):
result = []
for n in numbers:
result.append(n * 2)
return result # was missing

This is the most common None bug in Python. The programmer builds a result but forgets to return it. The function exits without a return statement, so CPython inserts LOAD_CONST None; RETURN_VALUE automatically. The caller receives None and crashes when trying to use it as a list.

def double_all(numbers):
    """Return a new list where each element is doubled.
    BUG: this function has a missing return.
    Fix it so it returns the doubled list.
    """
    result = []
    for n in numbers:
        result.append(n * 2)
    # BUG: missing return

# Do not modify the test code below
def test():
    output = double_all([1, 2, 3])
    assert output is not None, "Function returned None!"
    assert output == [2, 4, 6]
    return "All tests passed"
Expected Output
All tests passed
Hints

Hint 1: A function with no return statement implicitly returns None.

Hint 2: Add a return statement that sends the result list back to the caller.

#3Classify Return StylesEasy
returnreturn Noneimplicit-return

Write classify_return_style that calls a given function and returns 'returns_none' if the result is None, or 'returns_value' otherwise. Test it against four functions that use different return styles.

Python
def classify_return_style(func):
    result = func()
    if result is None:
        return 'returns_none'
    return 'returns_value'

def style_a():
    return None

def style_b():
    return

def style_c():
    pass

def style_d():
    return 42

print(f"style_a: {classify_return_style(style_a)}")
print(f"style_b: {classify_return_style(style_b)}")
print(f"style_c: {classify_return_style(style_c)}")
print(f"style_d: {classify_return_style(style_d)}")
Solution
def classify_return_style(func):
result = func()
if result is None:
return 'returns_none'
return 'returns_value'

All three None-returning styles are semantically identical. return None, bare return, and no return statement all cause the function to return the same None singleton. The convention difference is purely about communicating intent to readers: return None means None is a meaningful value, bare return means early exit, and no return means procedure.

def classify_return_style(func):
    """Call the given function and return a string:
    - 'returns_none' if the function returns None
    - 'returns_value' if the function returns something else
    """
    # TODO: implement
    pass

def style_a():
    return None

def style_b():
    return

def style_c():
    pass

def style_d():
    return 42
Expected Output
style_a: returns_none
style_b: returns_none
style_c: returns_none
style_d: returns_value
Hints

Hint 1: Call the function and check the result with `is None`.

Hint 2: All three styles — return None, bare return, and no return — produce the same None value.

#4Fix the In-Place Method BugEasy
sortin-placeCQSNone

The function get_sorted_unique is supposed to return a sorted list of unique elements. But it assigns the result of list.sort() — which is None. Fix the bug.

Python
def get_sorted_unique(items):
    result = sorted(set(items))
    return result

def test():
    output = get_sorted_unique([3, 1, 2, 3, 1])
    assert output == [1, 2, 3], f"Expected [1, 2, 3], got {output}"
    return "All tests passed"

print(test())
Solution
def get_sorted_unique(items):
result = sorted(set(items))
return result

# Alternative fix:
def get_sorted_unique_v2(items):
result = list(set(items))
result.sort() # sort in place, do NOT assign
return result

list.sort() returns None by design — this is the Command-Query Separation principle. Commands (mutations) return None to prevent you from assuming the original is unchanged. Use sorted() when you need a return value, or call .sort() on its own line when you want in-place mutation.

def get_sorted_unique(items):
    """Return a sorted list of unique items.
    BUG: this function chains in-place methods
    and returns None.
    Fix it without changing the function signature.
    """
    result = list(set(items)).sort()
    return result

# Do not modify test code
def test():
    output = get_sorted_unique([3, 1, 2, 3, 1])
    assert output == [1, 2, 3], f"Expected [1, 2, 3], got {output}"
    return "All tests passed"
Expected Output
All tests passed
Hints

Hint 1: list.sort() mutates in place and returns None — you cannot assign its return value.

Hint 2: Use sorted() which returns a new sorted list, or call sort() on a separate line.


Medium

#5Safe Identity CheckMedium
is None== None__eq__identity

Write a function that compares is None vs == None across a list of values. Return the indices that match under each check. The results will differ when an object overrides __eq__.

Python
def safe_none_check(values):
    identity = []
    equality = []
    for i, v in enumerate(values):
        if v is None:
            identity.append(i)
        if v == None:
            equality.append(i)
    return {'identity': identity, 'equality': equality}

class AlwaysEqual:
    def __eq__(self, other):
        return True

def test():
    items = [None, 0, "", AlwaysEqual(), False, None]
    result = safe_none_check(items)
    assert result['identity'] == [0, 5]
    assert result['equality'] == [0, 3, 5]
    return "All tests passed"

print(test())
Solution
def safe_none_check(values):
identity = []
equality = []
for i, v in enumerate(values):
if v is None:
identity.append(i)
if v == None:
equality.append(i)
return {'identity': identity, 'equality': equality}

== None gives a false positive for AlwaysEqual() at index 3 because its __eq__ returns True for everything. is None uses identity comparison (same memory address) and cannot be overridden. This is why PEP 8 mandates is None and is not None for all None checks.

def safe_none_check(values):
    """Given a list of values, return two lists:
    - 'identity': indices where value is None (using 'is')
    - 'equality': indices where value == None (using '==')
    
    Return a dict with keys 'identity' and 'equality'.
    The lists may differ when objects override __eq__.
    """
    # TODO: implement
    pass

class AlwaysEqual:
    def __eq__(self, other):
        return True

# Do not modify test code
def test():
    items = [None, 0, "", AlwaysEqual(), False, None]
    result = safe_none_check(items)
    assert result['identity'] == [0, 5]
    assert result['equality'] == [0, 3, 5]
    return "All tests passed"
Expected Output
All tests passed
Hints

Hint 1: Loop through with enumerate to track indices.

Hint 2: The AlwaysEqual class returns True for == None but False for is None — that is the whole point.

#6None-Safe PipelineMedium
None propagationguard clausepipeline

Build a function pipeline that chains transformations. If any function in the chain returns None, the pipeline short-circuits and returns None immediately, preventing NoneType errors downstream.

Python
def pipeline(value, *functions):
    current = value
    for func in functions:
        current = func(current)
        if current is None:
            return None
    return current

def test():
    double = lambda x: x * 2
    add_one = lambda x: x + 1
    explode = lambda x: None
    to_str = lambda x: str(x)

    assert pipeline(3, double, add_one) == 7
    assert pipeline(3, double, explode, to_str) is None
    assert pipeline(5) == 5
    return "All tests passed"

print(test())
Solution
def pipeline(value, *functions):
current = value
for func in functions:
current = func(current)
if current is None:
return None
return current

None propagation is a major source of production crashes. When a function returns None unexpectedly, the next function in the chain tries to operate on None and raises TypeError or AttributeError. This pipeline pattern — check after each step — is the manual equivalent of Optional chaining in other languages. The key is using is None (not if not current, which would also stop on 0, "", or False).

def pipeline(value, *functions):
    """Apply a chain of functions to a value.
    If any function returns None, stop the pipeline
    immediately and return None.
    Otherwise return the final transformed value.
    """
    # TODO: implement
    pass

# Do not modify test code
def test():
    double = lambda x: x * 2
    add_one = lambda x: x + 1
    explode = lambda x: None  # breaks the chain
    to_str = lambda x: str(x)

    assert pipeline(3, double, add_one) == 7
    assert pipeline(3, double, explode, to_str) is None
    assert pipeline(5) == 5
    return "All tests passed"
Expected Output
All tests passed
Hints

Hint 1: Loop through the functions, applying each to the current value.

Hint 2: After each application, check if the result is None — if so, short-circuit and return None.

#7Falsy vs None TrapMedium
truthinessfalsyNonebug

Fix the count_with_default function. The bug is using if not start: instead of if start is None:. When start=0 is passed, the falsy check incorrectly replaces it.

Python
def count_with_default(items, start=None):
    if start is None:
        start = 0
    return start + len(items)

def test():
    assert count_with_default([1, 2, 3]) == 3
    assert count_with_default([1, 2, 3], 10) == 13
    assert count_with_default([1, 2, 3], 0) == 3
    assert count_with_default([], 0) == 0
    assert count_with_default([], None) == 0
    return "All tests passed"

print(test())
Solution
def count_with_default(items, start=None):
if start is None: # was: if not start
start = 0
return start + len(items)

if not x catches every falsy valueNone, 0, 0.0, "", [], False, and any object whose __bool__ returns False. When you mean "was this argument omitted?", always use if x is None. The falsy trap is especially dangerous with numeric parameters where 0 is a perfectly valid input.

def count_with_default(items, start=None):
    """Count items in a list, starting from 'start'.
    If start is not provided (None), begin from 0.
    
    BUG: the current implementation uses 'if not start'
    which treats 0 as missing. Fix it.
    """
    if not start:
        start = 0
    return start + len(items)

# Do not modify test code
def test():
    assert count_with_default([1, 2, 3]) == 3
    assert count_with_default([1, 2, 3], 10) == 13
    assert count_with_default([1, 2, 3], 0) == 3  # BUG: returns 3 correctly but for wrong reason
    # The real test: start=0 should use 0, not replace it
    assert count_with_default([], 0) == 0  # Must be 0, not 0
    assert count_with_default([], None) == 0
    return "All tests passed"
Expected Output
All tests passed
Hints

Hint 1: `if not start` is True when start is 0, "", [], or False — not just None.

Hint 2: Use `if start is None` to only catch the "not provided" case.

#8Sentinel DisambiguatorMedium
sentinelobject()Nonecache

Implement a cache lookup that correctly distinguishes between "key exists with value None" and "key does not exist." Use a sentinel object so that None can be a valid cached value.

Python
_MISSING = object()

def smart_cache_get(cache, key, default=_MISSING):
    result = cache.get(key, _MISSING)
    if result is not _MISSING:
        return result
    if default is not _MISSING:
        return default
    raise KeyError(key)

def test():
    cache = {"a": 1, "b": None, "c": 0, "d": False}

    assert smart_cache_get(cache, "a") == 1
    assert smart_cache_get(cache, "b") is None
    assert smart_cache_get(cache, "c") == 0
    assert smart_cache_get(cache, "d") is False
    assert smart_cache_get(cache, "z", "fallback") == "fallback"
    assert smart_cache_get(cache, "z", None) is None

    try:
        smart_cache_get(cache, "z")
        assert False, "Should have raised KeyError"
    except KeyError:
        pass

    return "All tests passed"

print(test())
Solution
_MISSING = object()

def smart_cache_get(cache, key, default=_MISSING):
result = cache.get(key, _MISSING)
if result is not _MISSING:
return result
# Key is missing — check if caller provided a default
if default is not _MISSING:
return default
raise KeyError(key)

The sentinel pattern solves a fundamental ambiguity. With dict.get(key), both "key missing" and "key has value None" return None. By using _MISSING = object() as the fallback, we can distinguish the two cases. Each object() call creates a unique instance that equals nothing but itself. This is the same pattern used in Python's standard library (e.g., dataclasses.MISSING, inspect.Parameter.empty).

_MISSING = object()

def smart_cache_get(cache, key, default=_MISSING):
    """Look up key in cache dict.
    
    Return semantics:
    - If key exists, return its value (even if the value is None)
    - If key is missing and default is provided, return default
    - If key is missing and no default, raise KeyError
    
    You must distinguish 'cached None' from 'missing key'.
    """
    # TODO: implement
    pass

# Do not modify test code
def test():
    cache = {"a": 1, "b": None, "c": 0, "d": False}
    
    assert smart_cache_get(cache, "a") == 1
    assert smart_cache_get(cache, "b") is None  # cached None
    assert smart_cache_get(cache, "c") == 0
    assert smart_cache_get(cache, "d") is False
    assert smart_cache_get(cache, "z", "fallback") == "fallback"
    assert smart_cache_get(cache, "z", None) is None  # explicit default=None
    
    try:
        smart_cache_get(cache, "z")  # no default, should raise
        assert False, "Should have raised KeyError"
    except KeyError:
        pass
    
    return "All tests passed"
Expected Output
All tests passed
Hints

Hint 1: Use dict.get(key, _MISSING) to distinguish missing keys from keys with None values.

Hint 2: Check if the result is _MISSING — if so, either use the default or raise KeyError.


Hard

#9None-Safe Attribute ChainHard
None propagationgetattroptional chaining

Implement optional chaining for Python objects. Given a dot-separated path like "user.profile.email", traverse the attribute chain safely. If any intermediate attribute is None or does not exist, return the default value.

Python
_MISSING = object()

def safe_getattr(obj, path, default=None):
    current = obj
    for attr in path.split("."):
        if current is None:
            return default
        current = getattr(current, attr, _MISSING)
        if current is _MISSING:
            return default
    return current

class Profile:
    def __init__(self, email):
        self.email = email

class User:
    def __init__(self, profile):
        self.profile = profile

class Request:
    def __init__(self, user):
        self.user = user

def test():
    req1 = Request(User(Profile("[email protected]")))
    req2 = Request(User(None))
    req3 = Request(None)

    assert safe_getattr(req1, "user.profile.email") == "[email protected]"
    assert safe_getattr(req2, "user.profile.email") is None
    assert safe_getattr(req2, "user.profile.email", "n/a") == "n/a"
    assert safe_getattr(req3, "user.profile.email") is None
    assert safe_getattr(req1, "user") is req1.user
    assert safe_getattr(None, "anything") is None
    return "All tests passed"

print(test())
Solution
_MISSING = object()

def safe_getattr(obj, path, default=None):
current = obj
for attr in path.split("."):
if current is None:
return default
current = getattr(current, attr, _MISSING)
if current is _MISSING:
return default
return current

This is Python's manual equivalent of optional chaining (?. in JavaScript/TypeScript). The function combines two None-handling patterns: checking for None at each step (preventing AttributeError on None objects) and using a sentinel with getattr (preventing AttributeError on missing attributes). This pattern is extremely common in web frameworks — Django's template engine, for example, does exactly this when resolving variable lookups.

def safe_getattr(obj, path, default=None):
    """Safely traverse a dot-separated attribute path.
    
    If any attribute in the chain is None or missing,
    return the default instead of raising an error.
    
    Example:
      safe_getattr(request, 'user.profile.email')
    is equivalent to:
      request?.user?.profile?.email  (optional chaining)
    """
    # TODO: implement
    pass

# Do not modify test code
class Profile:
    def __init__(self, email):
        self.email = email

class User:
    def __init__(self, profile):
        self.profile = profile

class Request:
    def __init__(self, user):
        self.user = user

def test():
    req1 = Request(User(Profile("[email protected]")))
    req2 = Request(User(None))  # no profile
    req3 = Request(None)  # no user
    
    assert safe_getattr(req1, "user.profile.email") == "[email protected]"
    assert safe_getattr(req2, "user.profile.email") is None
    assert safe_getattr(req2, "user.profile.email", "n/a") == "n/a"
    assert safe_getattr(req3, "user.profile.email") is None
    assert safe_getattr(req1, "user") is req1.user
    assert safe_getattr(None, "anything") is None
    return "All tests passed"
Expected Output
All tests passed
Hints

Hint 1: Split the path on "." and traverse one attribute at a time.

Hint 2: At each step, if the current object is None, return the default immediately.

Hint 3: Use getattr with a sentinel to detect missing attributes without catching unrelated exceptions.

#10Command-Query AuditHard
CQSin-placeNoneaudit

Write a function that audits list methods to classify them as "command" (returns None, mutates in place) or "query" (returns a value). This demonstrates the CQS principle in Python's standard library.

Python
def audit_methods(obj, method_names):
    results = {}
    for name in method_names:
        obj_copy = list(obj)
        method = getattr(obj_copy, name, None)
        if method is None:
            results[name] = 'unknown'
            continue
        try:
            ret = method()
            if ret is None:
                results[name] = 'command'
            else:
                results[name] = 'query'
        except TypeError:
            results[name] = 'unknown'
    return results

def test():
    methods = ['sort', 'reverse', 'copy', 'clear']
    result = audit_methods([3, 1, 2], methods)
    assert result['sort'] == 'command'
    assert result['reverse'] == 'command'
    assert result['copy'] == 'query'
    assert result['clear'] == 'command'
    return "All tests passed"

print(test())
Solution
def audit_methods(obj, method_names):
results = {}
for name in method_names:
obj_copy = list(obj) # fresh copy each time
method = getattr(obj_copy, name, None)
if method is None:
results[name] = 'unknown'
continue
try:
ret = method()
if ret is None:
results[name] = 'command'
else:
results[name] = 'query'
except TypeError:
results[name] = 'unknown'
return results

The critical insight is copying before each call. sort() and reverse() mutate the list and return None (commands). copy() returns a new list (query). clear() empties the list and returns None (command). Without copying, the first sort() call would mutate the original, and clear() would empty it before subsequent tests. This audit confirms that Python's list API consistently follows CQS — every mutating method returns None.

def audit_methods(obj, method_names):
    """Test each method on a COPY of obj to determine
    if it follows Command-Query Separation.
    
    For each method_name, call it on a copy of obj
    (to avoid mutation issues) and classify:
    - 'command': method returns None (mutates in place)
    - 'query': method returns non-None (returns new value)
    
    Return a dict mapping method_name -> 'command' or 'query'.
    
    Note: call methods with no arguments beyond self.
    Some methods may need a dummy arg — skip those that
    raise TypeError and classify as 'unknown'.
    """
    # TODO: implement
    pass

# Do not modify test code
def test():
    methods = ['sort', 'reverse', 'copy', 'clear']
    result = audit_methods([3, 1, 2], methods)
    assert result['sort'] == 'command'
    assert result['reverse'] == 'command'
    assert result['copy'] == 'query'
    assert result['clear'] == 'command'
    return "All tests passed"
Expected Output
All tests passed
Hints

Hint 1: Use list() or copy() to create a fresh copy before each method call — mutations persist.

Hint 2: getattr(obj_copy, method_name)() calls the method. Wrap in try/except for methods that need arguments.

#11Maybe MonadHard
Nonemonadfunctionalchaining

Implement a Maybe monad that wraps potentially-None values and allows safe chaining. bind chains functions that return Maybe, map chains functions that return plain values, and or_else extracts the final result with a fallback.

Python
class Maybe:
    def __init__(self, value):
        self._value = value

    def bind(self, func):
        if self._value is None:
            return Maybe(None)
        return func(self._value)

    def map(self, func):
        if self._value is None:
            return Maybe(None)
        return Maybe(func(self._value))

    def or_else(self, default):
        if self._value is None:
            return default
        return self._value

    def is_nothing(self):
        return self._value is None

    def __repr__(self):
        if self._value is None:
            return "Nothing"
        return f"Maybe({self._value!r})"

def test():
    def safe_div(a, b):
        if b == 0:
            return Maybe(None)
        return Maybe(a / b)

    result = (Maybe(10)
        .bind(lambda x: safe_div(x, 2))
        .map(lambda x: x + 1)
        .or_else(0))
    assert result == 6.0

    result = (Maybe(10)
        .bind(lambda x: safe_div(x, 0))
        .map(lambda x: x + 1)
        .or_else(0))
    assert result == 0

    result = (Maybe(None)
        .map(lambda x: x * 2)
        .or_else(-1))
    assert result == -1

    assert Maybe(42).is_nothing() is False
    assert Maybe(None).is_nothing() is True
    assert repr(Maybe(5)) == "Maybe(5)"
    assert repr(Maybe(None)) == "Nothing"

    return "All tests passed"

print(test())
Solution
class Maybe:
def __init__(self, value):
self._value = value

def bind(self, func):
if self._value is None:
return Maybe(None)
return func(self._value)

def map(self, func):
if self._value is None:
return Maybe(None)
return Maybe(func(self._value))

def or_else(self, default):
if self._value is None:
return default
return self._value

def is_nothing(self):
return self._value is None

def __repr__(self):
if self._value is None:
return "Nothing"
return f"Maybe({self._value!r})"

The Maybe monad eliminates nested None checks entirely. Instead of writing if x is not None: if y is not None: if z is not None:, you write Maybe(x).map(f).map(g).map(h).or_else(default). The key distinction: bind expects the function to return a Maybe (for operations that might fail), while map wraps the result automatically (for operations that always succeed on non-None input). This pattern is native in Haskell, Rust (Option), and Swift (Optional), but in Python you typically use explicit is None checks — the Maybe monad is a useful exercise for understanding why those languages built it in.

class Maybe:
    """A Maybe monad that wraps a value which might be None.
    Enables safe chaining of operations without manual
    None checks at every step.
    
    Implement:
    - Maybe(value): wrap a value
    - .bind(func): apply func if value is not None,
      func must return a Maybe
    - .map(func): apply func if value is not None,
      func returns a plain value (auto-wrapped)
    - .or_else(default): return value if not None,
      otherwise return default
    - .is_nothing(): True if wrapped value is None
    """
    def __init__(self, value):
        # TODO: implement
        pass

    def bind(self, func):
        # TODO: implement
        pass

    def map(self, func):
        # TODO: implement
        pass

    def or_else(self, default):
        # TODO: implement
        pass

    def is_nothing(self):
        # TODO: implement
        pass

    def __repr__(self):
        # TODO: implement
        pass

# Do not modify test code
def test():
    # Safe division
    def safe_div(a, b):
        if b == 0:
            return Maybe(None)
        return Maybe(a / b)

    # Chain operations
    result = (Maybe(10)
        .bind(lambda x: safe_div(x, 2))
        .map(lambda x: x + 1)
        .or_else(0))
    assert result == 6.0

    # None propagation
    result = (Maybe(10)
        .bind(lambda x: safe_div(x, 0))
        .map(lambda x: x + 1)
        .or_else(0))
    assert result == 0

    # Start with None
    result = (Maybe(None)
        .map(lambda x: x * 2)
        .or_else(-1))
    assert result == -1

    assert Maybe(42).is_nothing() is False
    assert Maybe(None).is_nothing() is True
    assert repr(Maybe(5)) == "Maybe(5)"
    assert repr(Maybe(None)) == "Nothing"

    return "All tests passed"
Expected Output
All tests passed
Hints

Hint 1: In bind and map, check if self._value is None — if so, return Maybe(None) without calling func.

Hint 2: bind expects func to return a Maybe, while map wraps the result in Maybe automatically.

Hint 3: or_else should return the raw value, not a Maybe — it is the "escape hatch" from the wrapper.

© 2026 EngineersOfAI. All rights reserved.