ruff UP024
labelled unknown
Analysis Nemotron 3 Super
Flags usage of exception classes that are aliases of OSError (e.g., IOError, EnvironmentError, WindowsError) and recommends replacing them with OSError.
Fix: Replaces any occurrence of an OSError-alias exception name (such as IOError, EnvironmentError, WindowsError) with OSError in raise statements, except clauses, isinstance/issubclass checks, and similar contexts, preserving all surrounding syntax and structure.
11 hazards
- Rewriting `except IOError:` to `except OSError:` changes the set of caught exceptions if other OSError subclasses (like FileNotFoundError) were previously excluded by a broader except clause above, altering control flow.
- In `except (IOError, ValueError):`, replacing IOError with OSError may cause ValueError to be unreachable if OSError is checked first, due to exception matching order changing subclass precedence.
- If `IOError` is imported via `from errno import IOError` (though incorrect, hypothetically), rewriting to `OSError` would break the name resolution since `OSError` is not defined in that module's namespace.
- In a context like `except IOError as e:`, if the variable `e` is later used to access attributes specific to the IOError alias (none exist, but hypothetically), the rewrite assumes identical interface, which while true now, relies on the alias preserving all behavior.
- When `WindowsError` is used in `raise WindowsError`, rewriting to `raise OSError` loses the platform-specific signal that the error originated from Windows, though the exception instance behaves identically.
- In `issubclass(SomeErr, EnvironmentError)`, replacing EnvironmentError with OSError may return False if SomeErr is a subclass of a removed alias that no longer inherits from OSError in a future Python version, making the fix forward-incompatible if the alias is deleted.
- If a comment like `# Prefer IOError for clarity` appears after `raise IOError`, the autofix may preserve the comment but it becomes misleading post-rewrite, constituting a documentation hazard.
- In `try: ... except IOError: ... finally:`, the rewrite to `OSError` does not affect finally execution, but if the except block was relying on IOError being a distinct type for logging or metrics, the semantics of error categorization change.
- When `EnvironmentError` is used in a tuple like `except (EnvironmentError, KeyboardInterrupt):`, replacing it with `OSError` risks catching KeyboardInterrupt less likely if OSError is checked before KeyboardInterrupt in the tuple, though tuple order is preserved so this only matters if the interpreter reorders—which it doesn't, but the hazard is perceived reordering risk.
- If `IOError` is dynamically looked up via `getattr(exceptions_module, 'IOError')` and the rewrite incorrectly modifies the string literal, it would break runtime attribute access, though the rule does not target string literals—this hazard applies only if the autofix erroneously extends beyond identifiers.
- In a class definition like `class CustomIOError(IOError):`, rewriting the base to `OSError` is correct and safe, but if the class relied on IOError being a distinct type for `__instancecheck__` overrides, the change could break custom behavior, though no such override exists in standard aliases.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fix ok | Comments attached to rewritten code | comments and docstrings attached to the removed or moved code |
| r1-p002 | silent prediction off | Custom subclass inheriting from alias | In a class definition like `class CustomIOError(IOError):`, rewriting the base to `OSError` is correct and safe, but if the class relied on IOError being a distinct type for `__instancecheck__` overri |
| r1-p003 | fix ok | Tuple order in except clause with KeyboardInterrupt | When `EnvironmentError` is used in a tuple like `except (EnvironmentError, KeyboardInterrupt):`, replacing it with `OSError` risks catching KeyboardInterrupt less likely if OSError is checked before K |
| r1-p004 | fix ok | Unreachable except clause due to subclass precedence | In `except (IOError, ValueError):`, replacing IOError with OSError may cause ValueError to be unreachable if OSError is checked first, due to exception matching order changing subclass precedence. |
| r1-p005 | fix ok | Exception chaining in finally/else clauses | exceptions raised inside the rewritten code, finally/else clauses, exception chaining |
| r1-p006 | silent prediction off | pathlib vs os.path behavior on relative paths | pathlib vs os.path behavior on relative paths, symlinks, missing files, bytes paths |
| r1-p007 | silent prediction off | Broader except above narrowing control flow | Rewriting `except IOError:` to `except OSError:` changes the set of caught exceptions if other OSError subclasses (like FileNotFoundError) were previously excluded by a broader except clause above, al |
| r1-p008 | silent prediction off | Evaluation order and side effects in expressions | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r2-p001 | silent | Dynamic lookup via getattr | If autofix erroneously rewrites string literals like 'IOError' during dynamic attribute access, it breaks getattr-based lookups. |
| r2-p002 | fix ok | Async function with await ordering | Rewriting exception types in async contexts should not interfere with await execution order or generator state. |
| r2-p003 | fix ok | Finally block reliance on exception type for logging | Changing except IOError to except OSError alters which exceptions are caught, affecting whether finally runs after a handled exception vs propagating. |
| r2-p004 | fix ok | Class attribute descriptor with side effects on access | If the rewritten exception type is used in a descriptor's __get__ with side effects, changing it could alter when/ how many times the descriptor fires. |
| r2-p005 | silent prediction off | Keyword-only args with default evaluation once | Default values for keyword-only args are evaluated once at def-time; if the rewritten expression is in a default, it could capture stale state. |
| r2-p006 | fix ok | Generator vs list consumption laziness | If the rewritten expression appears in a generator condition, changing it could alter when the generator yields vs returns, affecting iteration timing. |
| r2-p007 | fix ok | Broad except above narrowing control flow with subclass precedence | Rewriting except IOError to except OSError in a narrow handler above a broad OSError handler can cause the broad handler to catch exceptions intended for the narrow one, reversing control flow. |
| r2-p008 | fix ok prediction off | String formatting edge cases with % and .format | If the autofix touches string literals near exception handlers (e.g., in log messages), it could corrupt formatting directives or nested braces. |
Sources
r1-p001 · Comments attached to rewritten code
1# This comment explains why we handle IOError
2# Specifically for legacy file operations
3raise IOError("legacy error") # Keep this message
--- orig.py 2026-10-17 17:18:53.256973814 -0400 +++ fixed.py 2026-10-17 17:18:53.645953769 -0400 @@ -1,3 +1,3 @@ # This comment explains why we handle IOError # Specifically for legacy file operations -raise IOError("legacy error") # Keep this message +raise OSError("legacy error") # Keep this message
r1-p002 · Custom subclass inheriting from alias
1class CustomIOError(IOError): # RULE SHOULD REWRITE BASE TO OSError
2 pass
3
4try:
5 raise CustomIOError("custom")
6except OSError as e:
7 print(f"Caught: {type(e).__name__}")
8except Exception as e:
9 print(f"Unexpected: {type(e).__name__}")
Caught: CustomIOError
r1-p003 · Tuple order in except clause with KeyboardInterrupt
1import sys
2try:
3 raise KeyboardInterrupt("user abort")
4except (EnvironmentError, KeyboardInterrupt) as e: # RULE REWRITES EnvironmentError -> OSError
5 print(f"Caught: {type(e).__name__}: {e}")
--- orig.py 2026-10-17 17:18:53.989936042 -0400 +++ fixed.py 2026-10-17 17:18:54.144928068 -0400 @@ -1,5 +1,5 @@ import sys try: raise KeyboardInterrupt("user abort") -except (EnvironmentError, KeyboardInterrupt) as e: # RULE REWRITES EnvironmentError -> OSError +except (OSError, KeyboardInterrupt) as e: # RULE REWRITES EnvironmentError -> OSError print(f"Caught: {type(e).__name__}: {e}")
Caught: KeyboardInterrupt: user abort
r1-p004 · Unreachable except clause due to subclass precedence
1try:
2 raise ValueError("value issue")
3except (IOError, ValueError) as e: # RULE REWRITES IOError -> OSError; OSError is broader, but ValueError not subclass -> still caught
4 print(f"Caught: {type(e).__name__}")
--- orig.py 2026-10-17 17:18:54.246922821 -0400 +++ fixed.py 2026-10-17 17:18:54.409914436 -0400 @@ -1,4 +1,4 @@ try: raise ValueError("value issue") -except (IOError, ValueError) as e: # RULE REWRITES IOError -> OSError; OSError is broader, but ValueError not subclass -> still caught +except (OSError, ValueError) as e: # RULE REWRITES IOError -> OSError; OSError is broader, but ValueError not subclass -> still caught print(f"Caught: {type(e).__name__}")
Caught: ValueError
r1-p005 · Exception chaining in finally/else clauses
1try:
2 raise IOError("original") # RULE REWRITES IOError -> OSError
3except OSError as e:
4 try:
5 raise ValueError("chained") from e
6 finally:
7 print("finally executed")
8else:
9 print("no exception")
--- orig.py 2026-10-17 17:18:54.513909086 -0400 +++ fixed.py 2026-10-17 17:18:54.919888202 -0400 @@ -1,5 +1,5 @@ try: - raise IOError("original") # RULE REWRITES IOError -> OSError + raise OSError("original") # RULE REWRITES IOError -> OSError except OSError as e: try: raise ValueError("chained") from e
finally executed
r1-p006 · pathlib vs os.path behavior on relative paths
1import os
2import pathlib
3try:
4 pathlib.Path("nonexistent.txt").read_text()
5except OSError as e: # RULE MAY HAVE COME FROM EnvironmentError or similar
6 print(f"pathlib: {type(e).__name__}")
7try:
8 open("nonexistent.txt").read()
9except OSError as e:
10 print(f"open: {type(e).__name__}")
pathlib: FileNotFoundError open: FileNotFoundError
r1-p007 · Broader except above narrowing control flow
1try:
2 raise FileNotFoundError("missing")
3except OSError:
4 print("caught by OSError")
5except FileNotFoundError as e: # THIS IS NOW UNREACHABLE if above except IOError was rewritten to OSError
6 print("caught by FileNotFoundError")
caught by OSError
r1-p008 · Evaluation order and side effects in expressions
1def side_effect():
2 print("side effect")
3 return IOError
4
5try:
6 raise side_effect()("delayed") # RULE REWRITES the returned IOError -> OSError; side effect must happen
7except OSError as e:
8 print(f"Caught after side effect: {type(e).__name__}")
side effect Caught after side effect: OSError
r2-p001 · Dynamic lookup via getattr
1import builtins
2mod = builtins
3err = getattr(mod, 'IOError')
4try:
5 raise err
6except OSError as e:
7 print('Caught:', type(e).__name__)
8except Exception as e:
9 print('Unexpected:', type(e).__name__)
Caught: OSError
r2-p002 · Async function with await ordering
1async def foo():
2 try:
3 await bar()
4 except IOError as e:
5 print('caught IOError:', e)
6 except OSError as e:
7 print('caught OSError:', e)
8async def bar():
9 raise IOError('disk error')
10import asyncio
11asyncio.run(foo())
--- orig.py 2026-10-17 17:19:01.371558476 -0400 +++ fixed.py 2026-10-17 17:19:01.683542626 -0400 @@ -1,11 +1,11 @@ async def foo(): try: await bar() - except IOError as e: + except OSError as e: print('caught IOError:', e) except OSError as e: print('caught OSError:', e) async def bar(): - raise IOError('disk error') + raise OSError('disk error') import asyncio asyncio.run(foo())
caught IOError: disk error
r2-p003 · Finally block reliance on exception type for logging
1logged = []
2def log_exc(t):
3 logged.append(t)
4try:
5 raise FileNotFoundError('missing')
6except IOError:
7 log_exc('IOError-handled')
8except OSError as e:
9 log_exc('OSError-handled:', e.__class__.__name__)
10finally:
11 log_exc('finally')
12print('log:', logged)
--- orig.py 2026-10-17 17:19:01.855533888 -0400 +++ fixed.py 2026-10-17 17:19:02.017525661 -0400 @@ -3,7 +3,7 @@ logged.append(t) try: raise FileNotFoundError('missing') -except IOError: +except OSError: log_exc('IOError-handled') except OSError as e: log_exc('OSError-handled:', e.__class__.__name__)
log: ['IOError-handled', 'finally']
r2-p004 · Class attribute descriptor with side effects on access
1class ExcSelector:
2 def __get__(self, obj, owner):
3 print('ExcSelector accessed')
4 return IOError
5class Test:
6 exc = ExcSelector()
7try:
8 raise Test.exc('fail')
9except IOError as e:
10 print('caught:', e)
11except OSError as e:
12 print('caught OS:', e)
--- orig.py 2026-10-17 17:19:02.121520387 -0400 +++ fixed.py 2026-10-17 17:19:02.281512273 -0400 @@ -6,7 +6,7 @@ exc = ExcSelector() try: raise Test.exc('fail') -except IOError as e: +except OSError as e: print('caught:', e) except OSError as e: print('caught OS:', e)
ExcSelector accessed caught: fail
r2-p005 · Keyword-only args with default evaluation once
1def make_handler(*,
2 exc_type=IOError,
3 msg='default'):
4 try:
5 raise exc_type(msg)
6 except exc_type as e:
7 return f'handled {type(e).__name__}'
8 except OSError as e:
9 return f'OSError fallback {type(e).__name__}'
10print(make_handler())
11print(make_handler(exc_type=OSError))
handled OSError handled OSError
r2-p006 · Generator vs list consumption laziness
1def gen():
2 for i in range(3):
3 if i == 1:
4 raise IOError('at 1')
5 yield i
6g = gen()
7try:
8 for x in g:
9 print('yielded:', x)
10except IOError as e:
11 print('gen IOError:', e)
12except OSError as e:
13 print('gen OSError:', e)
--- orig.py 2026-10-17 17:19:02.521500103 -0400 +++ fixed.py 2026-10-17 17:19:02.682491939 -0400 @@ -1,13 +1,13 @@ def gen(): for i in range(3): if i == 1: - raise IOError('at 1') + raise OSError('at 1') yield i g = gen() try: for x in g: print('yielded:', x) -except IOError as e: +except OSError as e: print('gen IOError:', e) except OSError as e: print('gen OSError:', e)
yielded: 0 gen IOError: at 1
r2-p007 · Broad except above narrowing control flow with subclass precedence
1try:
2 raise FileNotFoundError('no such file')
3except IOError:
4 print('narrow IOError')
5except OSError:
6 print('broad OSError')
7except Exception as e:
8 print('unexpected:', type(e).__name__)
--- orig.py 2026-10-17 17:19:02.786486665 -0400 +++ fixed.py 2026-10-17 17:19:02.948478450 -0400 @@ -1,6 +1,6 @@ try: raise FileNotFoundError('no such file') -except IOError: +except OSError: print('narrow IOError') except OSError: print('broad OSError')
narrow IOError
r2-p008 · String formatting edge cases with % and .format
1try:
2 raise IOError('error %d' % 42)
3except IOError as e:
4 print('msg:', e)
5except OSError as e:
6 print('OSError msg:', str(e))
7try:
8 raise EnvironmentError('{0}!')
9except EnvironmentError as e:
10 print('fmt:', e.format() if hasattr(e, 'format') else str(e))
11except OSError as e:
12 print('OSError fmt:', str(e))
--- orig.py 2026-10-17 17:19:03.048473384 -0400 +++ fixed.py 2026-10-17 17:19:03.219464728 -0400 @@ -1,12 +1,12 @@ try: - raise IOError('error %d' % 42) -except IOError as e: + raise OSError('error %d' % 42) +except OSError as e: print('msg:', e) except OSError as e: print('OSError msg:', str(e)) try: - raise EnvironmentError('{0}!') -except EnvironmentError as e: + raise OSError('{0}!') +except OSError as e: print('fmt:', e.format() if hasattr(e, 'format') else str(e)) except OSError as e: print('OSError fmt:', str(e))
msg: error 42
fmt: {0}!