Stevoisiak
Stevoisiak

Reputation: 26895

How do I safely iterate over an optional list in Python?

I have a function which compares a file path against two lists of folder paths. It returns true if the start of the path matches any folder from list A (include) but doesn't match an optional list B (exclude).

from typing import List, Optional

def match(file: str, include: List[str], exclude: Optional[List[str]]=None):
    return any(file.startswith(p) for p in include) and not any(
        file.startswith(p) for p in exclude
    )

The function works as expected if all parameter values are provided, but fails with a TypeError if no exclude folders are given.

# True
match(file="source/main.py", include=["source/", "output/"], exclude=["output/debug/"])

# False
match(file="output/debug/output.log", include=["source/", "output/"], exclude=["output/debug/"])

# Expected result - True
# Actual result - TypeError: 'NoneType' object is not iterable
match(file="output/debug/output.log", include=["source/", "output/"])

There was a PEP proposal to introduce null-aware operators that may have helped, but it has not been implemented as of now.

How can I safely iterate over an Optional list in Python without running into null errors?

Upvotes: 0

Views: 1180

Answers (3)

Stevoisiak
Stevoisiak

Reputation: 26895

Use an or condition to check if exclude is None before iterating through it.

from typing import List, Optional

def match(file: str, include: List[str], exclude: Optional[List[str]]=None):
    return any(file.startswith(p) for p in include) and (
        exclude is None or not any(file.startswith(p) for p in exclude)
    )

# True
match(file="output/debug/output.log", include=["source/", "output/"])

Upvotes: 0

chepner
chepner

Reputation: 532313

You don't seem to care if exclude is really a list, just that it's iterable. In that case, you can change the default to an immutable empty tuple if you change the type to Iterable[str].

from typing import Iterable


def match(file: str, include: Iterable[str], exclude:Iterable[str]=()):
    return (any(file.startswith(p) for p in include) 
            and all(not file.startswith(p) for p in exclude)

This also requires switching not any(... for p in exclude) to all(not ... for p in exclude) so that the test is vacuously true for the default value of exclude.

Upvotes: 1

JJC
JJC

Reputation: 10033

Just use the "or []" technique to ensure "exclude" is only iterated when provided.

from typing import List, Optional

def match(file: str, include: List[str], exclude: Optional[List[str]]=None):
    return any(file.startswith(p) for p in include) and not any(
        file.startswith(p) for p in exclude or []
    )

>>> match('foo', ['f',])
True
>>> match('foo', ['f',], ['foo'])
False

Upvotes: 1

Related Questions