ruff PLR1701
labelled unknown
Analysis Nemotron 3 Super
The rule flags repeated isinstance() checks on the same variable combined with logical OR operators.
Fix: Replace a chain of isinstance(var, T1) or isinstance(var, T2) or ... with isinstance(var, (T1, T2, ...)) for Python <3.10, or isinstance(var, T1 | T2 | ...) for Python >=3.10 when target-version allows it.
10 hazards
- Rewriting isinstance(var, T1 | T2) fails at runtime if T1 or T2 is a tuple, e.g., T = (int, str); isinstance(x, T | float) raises TypeError.
- If the variable in isinstance checks is not identical (e.g., due to shadowing or aliasing), merging changes which object is tested.
- Side effects in isinstance arguments (like function calls) are evaluated once instead of per clause, altering behavior if those functions have side effects.
- Comments embedded within the isinstance chain (e.g., isinstance(x, # type int) or ...) may be lost during rewrite due to token reformatting.
- In nested expressions, precedence changes may occur if the original chain relied on 'or' short-circuiting with non-boolean contexts.
- When used in annotation contexts or stringized annotations, the rewritten form may not be syntactically valid or may change meaning.
- If the variable is a property with a getter, merging reduces getter calls from N to 1, altering side effects or state-dependent behavior.
- Macros or decorators that rely on the syntactic form of multiple isinstance calls may malfunction after rewrite.
- In eval() or exec() contexts with restricted namespaces, the tuple or union form may resolve names differently than sequential lookups.
- If one isinstance check uses a forward reference as a string and others don't, the merged tuple/union form may inconsistently handle string resolution.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fired, no fix | Runtime-evaluated type annotation | type annotations evaluated at runtime, typing-only imports |
| r1-p002 | fired, no fix | Decorator relying on syntactic form | Macros or decorators that rely on the syntactic form of multiple isinstance calls may malfunction after rewrite. |
| r1-p003 | fired, no fix | Variable shadowing in isinstance chain | If the variable in isinstance checks is not identical (e.g., due to shadowing or aliasing), merging changes which object is tested. |
| r1-p004 | invalid | Exception handling with finally/else | exceptions raised inside the rewritten code, finally/else clauses, exception chaining |
| r1-p005 | fired, no fix | Comments lost during rewrite | Comments embedded within the isinstance chain (e.g., isinstance(x, # type int) or ...) may be lost during rewrite due to token reformatting. |
| r1-p006 | fired, no fix | Side effects in isinstance arguments | Side effects in isinstance arguments (like function calls) are evaluated once instead of per clause, altering behavior if those functions have side effects. |
| r1-p007 | fired, no fix | Evaluation order and side effects | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r1-p008 | silent prediction off | Class vs instance attributes with descriptors | class attributes vs instance attributes, descriptors, properties with side effects |
| r2-p001 | fired, no fix | Tuple union rewrite fails | Rewriting isinstance(var, T1 | T2) fails at runtime if T1 or T2 is a tuple |
| r2-p002 | silent prediction off | Walrus scoping in comprehension | walrus, comprehensions scoping, late binding in closures and lambdas |
| r2-p003 | silent prediction off | Side effects in isinstance args | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r2-p004 | fired, no fix | Precedence change in nested or | In nested expressions, precedence changes may occur if the original chain relied on 'or' short-circuiting with non-boolean contexts. |
| r2-p005 | fired, no fix | Mutable default aliasing | mutable default arguments and aliasing after a rewrite |
| r2-p006 | silent | Context manager without close | context managers that are not files, objects with write() but no close() |
| r2-p007 | invalid | Comment loss in isinstance chain | Comments embedded within the isinstance chain may be lost during rewrite due to token reformatting. |
| r2-p008 | fired, no fix | Bytes vs str encoding | bytes vs str, encoding and newline handling, text vs binary modes |
Sources
r1-p001 · Runtime-evaluated type annotation
1import typing
2if typing.TYPE_CHECKING:
3 x = []
4else:
5 x = [1]
6print(isinstance(x, list) or isinstance(x, typing.List))
True
r1-p002 · Decorator relying on syntactic form
1def check_source(func):
2 import inspect
3 src = inspect.getsource(func)
4 return 'isinstance' in src and src.count('isinstance') == 2
5@check_source
6def f(val):
7 return isinstance(val, int) or isinstance(val, str)
8print(f(1))
r1-p003 · Variable shadowing in isinstance chain
1x = 5
2def g():
3 x = 'hello'
4 return isinstance(x, int) or isinstance(x, str)
5print(g())
True
r1-p004 · Exception handling with finally/else (generated program does not parse: Traceback (most recent call last):)
1try:
2 result = isinstance(1/0, int) or isinstance(1/0, str)
3except ZeroDivisionError as e:
4 print('caught:', type(e).__name__)
5finally:
6 print('finally')
7else:
8 print('else:', result)
r1-p005 · Comments lost during rewrite
1x = 42
2# isinstance(x, int) # first check
3# isinstance(x, str) # second check
4result = isinstance(x, int) or isinstance(x, str) # keep this line
5print(result)
True
r1-p006 · Side effects in isinstance arguments
1counter = 0
2def get_type():
3 global counter
4 counter += 1
5 return int if counter == 1 else str
6x = 5
7print(isinstance(x, get_type()) or isinstance(x, get_type()))
8print('counter:', counter)
True counter: 1
r1-p007 · Evaluation order and side effects
1def side_effect(label):
2 print('eval:', label)
3 return int
4x = 'hi'
5print(isinstance(x, side_effect('first')) or isinstance(x, side_effect('second')))
eval: first eval: second False
r1-p008 · Class vs instance attributes with descriptors
1class A:
2 attr = 1
3 @property
4 def prop(self):
5 print('prop accessed')
6 return 2
7a = A()
8print(isinstance(a.attr, int) or isinstance(a.prop, int))
True
r2-p001 · Tuple union rewrite fails
1import sys
2T = (int, str)
3def check(v):
4 return isinstance(v, T) or isinstance(v, float)
5x = [1]
6try:
7 r = check(x)
8except Exception as e:
9 print(f'{type(e).__name__}: {e}')
10 sys.exit(0)
11print(r)
False
r2-p002 · Walrus scoping in comprehension
1def make_funcs():
2 fs = []
3 for i in range(2):
4 if (x := i) is not None and isinstance(x, int) or isinstance(x, str):
5 fs.append(lambda: x)
6 return fs
7fs = make_funcs()
8print([f() for f in fs])
[1, 1]
r2-p003 · Side effects in isinstance args
1counter = 0
2def next_int():
3 global counter
4 counter += 1
5 return counter
6a = next_int()
7b = next_int()
8if isinstance(a, int) or isinstance(b, str):
9 print('taken')
10else:
11 print('not taken')
12print(f'counter={counter}')
taken counter=2
r2-p004 · Precedence change in nested or
1def f(): return 0
2def g(): return 1
3result = f() or isinstance(0, int) or isinstance(0, str)
4print(result)
True
r2-p005 · Mutable default aliasing
1def func(x, cache=[]):
2 if isinstance(x, list) or isinstance(x, tuple):
3 cache.append(x)
4 return len(cache)
5 return -1
6print(func([1]))
7print(func((2,)))
8print(func([3]))
1 2 3
r2-p006 · Context manager without close
1class StdoutLike:
2 def write(self, s): pass
3out = StdoutLike()
4if hasattr(out, 'write') or hasattr(out, 'flush'):
5 out.write('hello')
6print('done')
done
r2-p007 · Comment loss in isinstance chain (generated program does not parse: Traceback (most recent call last):)
1x = 5
2# First check: int, then str
3if isinstance(x, int) # prefer int
4 or isinstance(x, str): # fallback
5 print('matched')
6else:
7 print('unmatched')
r2-p008 · Bytes vs str encoding
1data = b'hello\nworld'
2if isinstance(data, bytes) or isinstance(data, str):
3 print('is bytes or str')
4 if isinstance(data, bytes):
5 print(f'bytes len={len(data)}')
6 else:
7 print(f'str len={len(data)}')
8else:
9 print('neither')
is bytes or str bytes len=11