Skip to content

Commit 311b240

Browse files
committed
GCI109 rule - avoid exception for control flow
1 parent 798bac2 commit 311b240

File tree

2 files changed

+195
-0
lines changed

2 files changed

+195
-0
lines changed

src/main/rules/GCI109/GCI109.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"title": "Avoid Using Exceptions for Control Flow",
3+
"type": "CODE_SMELL",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "10min"
8+
},
9+
"tags": [
10+
"creedengo",
11+
"eco-design",
12+
"performance",
13+
"python",
14+
"bad-practice"
15+
],
16+
"defaultSeverity": "Info"
17+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
= GCI109 — Avoid Using Exceptions for Control Flow (Python)
2+
3+
== Why this rule?
4+
5+
Raising and catching an exception in Python is not the same as just using a simple if/else.
6+
In CPython, raising an exception triggers stack unwinding, creates and attaches a traceback
7+
object, and searches for a suitable handler; all of which cost CPU and memory, making it an expensive operation.
8+
Since Python 3.11, the interpreter has reduced overheads in normal execution, but actually raising
9+
an exception remains costly. This cost makes using exceptions for control flow a poor choice—especially in
10+
frequently-running code such as inside loops, where the expense quickly multiplies.
11+
12+
Why it matters for eco‑design: more CPU cycles → more energy → more carbon footprint.
13+
14+
== What is "control flow using exceptions"?
15+
16+
Using exceptions for control flow means relying on `try/except` to steer normal program execution
17+
(e.g., terminate loops, select a code path, or fetch defaults), instead of using idiomatic constructs
18+
like `if/else`, membership checks, iterator protocols (`for`, `next(it, default)`), or dedicated APIs
19+
(`dict.get`, `getattr` with a default, `enumerate`, bounds checks).
20+
21+
Why it’s not recommended:
22+
- Performance overhead: raising an exception creates exception and traceback objects and unwinds the stack.
23+
- Predictability: exceptions are optimized for rare, exceptional cases; frequent exceptions degrade throughput.
24+
- Readability and intent: `if/else`, iteration constructs, and "no-exception" APIs express intent more clearly.
25+
- Side effects and surprises: constructs like `hasattr` or properties may execute code and raise unrelated errors.
26+
27+
28+
== Rule scope
29+
30+
Flags "control‑flow" use of these exceptions:
31+
- KeyError, IndexError, AttributeError, StopIteration
32+
- Any use of exceptions to drive program flow rather than handle errors
33+
34+
Note: an `except` block that immediately re‑raises (bare `raise`) is error handling, not control flow.
35+
36+
== EAFP vs LBYL (when to prefer which)
37+
38+
Python encourages EAFP ("Easier to Ask Forgiveness than Permission"): try first, then handle the exception.
39+
EAFP is appropriate when failure is rare. When failure is frequent or expected,
40+
exceptions become an expensive branching mechanism; prefer LBYL or exception‑free idioms.
41+
42+
== Non-compliant examples
43+
44+
=== 1) Loop termination via StopIteration
45+
46+
[source,python]
47+
----
48+
it = iter(source)
49+
while True:
50+
try:
51+
item = next(it)
52+
except StopIteration:
53+
break # control flow via exception -> NO
54+
process(item)
55+
----
56+
57+
=== 2) Missing dict key
58+
59+
[source,python]
60+
----
61+
for user in users:
62+
try:
63+
plan = price_table[user.tier] # KeyError for control flow -> NO
64+
except KeyError:
65+
plan = DEFAULT_PLAN
66+
bill(user, plan)
67+
----
68+
69+
=== 3) "Missing" attribute
70+
71+
[source,python]
72+
----
73+
for obj in objects:
74+
try:
75+
feature = obj.attr # AttributeError for control flow -> NO
76+
except AttributeError:
77+
feature = fallback(obj)
78+
consume(feature)
79+
----
80+
81+
=== 4) Indexing with out-of-bounds
82+
83+
[source,python]
84+
----
85+
for i in range(n):
86+
try:
87+
x = data[i] # IndexError for control flow -> NO
88+
except IndexError:
89+
x = 0
90+
use(x)
91+
----
92+
93+
== Compliant alternatives (no "expected" exceptions)
94+
95+
=== 1) Clean iteration and termination
96+
97+
[source,python]
98+
----
99+
# Option A: idiomatic for-in
100+
for item in source:
101+
process(item)
102+
103+
# Option B: next(..., default) without exceptions
104+
it = iter(source)
105+
while True:
106+
item = next(it, None)
107+
if item is None:
108+
break
109+
process(item)
110+
----
111+
Why? PEP 479 forbids using StopIteration to drive control flow in generators;
112+
`for` or `next(..., default)` expresses the intent without raising exceptions.
113+
114+
=== 2) Dictionaries: prefer get / setdefault / defaultdict
115+
116+
[source,python]
117+
----
118+
for user in users:
119+
plan = price_table.get(user.tier, DEFAULT_PLAN)
120+
bill(user, plan)
121+
122+
# If you need to build structures on the fly:
123+
from collections import defaultdict
124+
counts = defaultdict(int)
125+
for key in keys:
126+
counts[key] += 1
127+
----
128+
129+
=== 3) Attributes: getattr with a default
130+
131+
[source,python]
132+
----
133+
for obj in objects:
134+
feature = getattr(obj, "attr", None) or fallback(obj)
135+
consume(feature)
136+
----
137+
Note: avoid `hasattr` on properties with side effects; `getattr(..., default)` does not raise.
138+
139+
=== 4) Indexing: bounds checks, enumerate, iterators
140+
141+
[source,python]
142+
----
143+
for i, x in enumerate(seq):
144+
use(x)
145+
146+
# or bounds checks when direct indexing is needed
147+
if 0 <= idx < len(seq):
148+
use(seq[idx])
149+
----
150+
151+
== Alternatives by exception (quick reference)
152+
153+
- KeyError: `dict.get(key, default)`, `defaultdict`, `setdefault`, pre‑check membership if needed (`if key in d:`).
154+
- IndexError: iterate (`for x in seq`), `enumerate`, bounds checks (`if 0 <= i < len(seq)`), slicing/`itertools.islice` for windowed access.
155+
- AttributeError: `getattr(obj, "name", default)`, design objects to expose explicit defaults or null‑object semantics.
156+
- StopIteration: use `for item in iterable`, or `next(it, sentinel)`; in async contexts, prefer `async for` and sentinels instead of relying on `StopAsyncIteration`.
157+
158+
== When exceptions are the right tool
159+
160+
- Truly exceptional errors (I/O, network, contract violations).
161+
- You enrich context and re‑raise (bare `raise`); this is error handling, not control flow.
162+
163+
== Eco-performance intuition
164+
165+
- Try without exception: near‑zero overhead in normal execution (3.11+ improvements).
166+
- Raised exception: creates exception + traceback objects and unwinds the stack → extra CPU cycles → more energy → more CO₂ (electricity as carbon proxy).
167+
- Minimizing expected exceptions reduces runtime and energy.
168+
169+
== References
170+
171+
172+
- What's New in Python 3.11 (Faster CPython): https://docs.python.org/3/whatsnew/3.11.html#faster-cpython
173+
- PEP 479 — StopIteration Handling: https://peps.python.org/pep-0479/
174+
- Real Python — Exceptions and EAFP discussion: https://realpython.com/python-exceptions/
175+
- Don't Use Exceptions For Flow Control : https://wiki.c2.com/?DontUseExceptionsForFlowControl
176+
- The Cost of except in Python : https://dlecocq.github.io/blog/2012/01/08/the-cost-of-except-in-python/
177+
- Exception handling performance regression in Python (bugs.python.org Issue40222): https://bugs.python.org/issue40222?
178+
- How fast are exceptions? : https://docs.python.org/2/faq/design.html#how-fast-are-exceptions

0 commit comments

Comments
 (0)