fixhunt

ruff PLR1701

labelled unknown
Runs / 2026-10-17T21-00-41-466Z-sjlk-ruff-PLR1701ruff 0.17.12026-10-17 21:00 UTC8 programs × 2 round(s)16 programs, 10 fired, 0 fixed, 0 findings, 2 rejectedmodel cost $0.0029

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

idresulttitlehazard
r1-p001fired, no fixRuntime-evaluated type annotationtype annotations evaluated at runtime, typing-only imports
r1-p002fired, no fixDecorator relying on syntactic formMacros or decorators that rely on the syntactic form of multiple isinstance calls may malfunction after rewrite.
r1-p003fired, no fixVariable shadowing in isinstance chainIf the variable in isinstance checks is not identical (e.g., due to shadowing or aliasing), merging changes which object is tested.
r1-p004invalidException handling with finally/elseexceptions raised inside the rewritten code, finally/else clauses, exception chaining
r1-p005fired, no fixComments lost during rewriteComments embedded within the isinstance chain (e.g., isinstance(x, # type int) or ...) may be lost during rewrite due to token reformatting.
r1-p006fired, no fixSide effects in isinstance argumentsSide effects in isinstance arguments (like function calls) are evaluated once instead of per clause, altering behavior if those functions have side effects.
r1-p007fired, no fixEvaluation order and side effectsevaluation order and side effects when expressions are merged, duplicated or deleted
r1-p008silent prediction offClass vs instance attributes with descriptorsclass attributes vs instance attributes, descriptors, properties with side effects
r2-p001fired, no fixTuple union rewrite failsRewriting isinstance(var, T1 | T2) fails at runtime if T1 or T2 is a tuple
r2-p002silent prediction offWalrus scoping in comprehensionwalrus, comprehensions scoping, late binding in closures and lambdas
r2-p003silent prediction offSide effects in isinstance argsevaluation order and side effects when expressions are merged, duplicated or deleted
r2-p004fired, no fixPrecedence change in nested orIn nested expressions, precedence changes may occur if the original chain relied on 'or' short-circuiting with non-boolean contexts.
r2-p005fired, no fixMutable default aliasingmutable default arguments and aliasing after a rewrite
r2-p006silentContext manager without closecontext managers that are not files, objects with write() but no close()
r2-p007invalidComment loss in isinstance chainComments embedded within the isinstance chain may be lost during rewrite due to token reformatting.
r2-p008fired, no fixBytes vs str encodingbytes 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