@sun-asterisk/sunlint 1.3.48 → 1.3.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/file-targeting-service.js +148 -15
- package/core/init-command.js +118 -70
- package/core/project-detector.js +517 -0
- package/core/tui-select.js +245 -0
- package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
- package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
- package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
- package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
- package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
- package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
- package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
- package/package.json +1 -1
- package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
- package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
- package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
- package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Specify Encoding When Opening Files
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Omitting encoding when opening text files causes silent failures on systems with non-UTF-8 locale, resulting in UnicodeDecodeError or garbled data in production.
|
|
5
|
+
tags: python, encoding, files, portability, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Specify Encoding When Opening Files
|
|
9
|
+
|
|
10
|
+
When calling `open()` in text mode without an explicit `encoding` argument, Python uses the OS's default locale encoding. This is typically UTF-8 on Linux/macOS, but `cp1252` or similar on Windows. Code that works locally can silently fail or corrupt data in other environments.
|
|
11
|
+
|
|
12
|
+
Always pass `encoding="utf-8"` (or whatever the file format requires) explicitly.
|
|
13
|
+
|
|
14
|
+
**Incorrect:**
|
|
15
|
+
```python
|
|
16
|
+
# Relies on OS default — breaks on Windows or non-UTF-8 systems
|
|
17
|
+
with open("data.txt") as f:
|
|
18
|
+
content = f.read()
|
|
19
|
+
|
|
20
|
+
with open("output.csv", "w") as f:
|
|
21
|
+
f.write("café,prix\n")
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Correct:**
|
|
25
|
+
```python
|
|
26
|
+
with open("data.txt", encoding="utf-8") as f:
|
|
27
|
+
content = f.read()
|
|
28
|
+
|
|
29
|
+
with open("output.csv", "w", encoding="utf-8") as f:
|
|
30
|
+
f.write("café,prix\n")
|
|
31
|
+
|
|
32
|
+
# When reading binary, no encoding needed
|
|
33
|
+
with open("image.png", "rb") as f:
|
|
34
|
+
data = f.read()
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Pathlib equivalent (also requires explicit encoding):**
|
|
38
|
+
```python
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
content = Path("data.txt").read_text(encoding="utf-8")
|
|
42
|
+
Path("output.txt").write_text("hello", encoding="utf-8")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Tools:** Ruff `W1514` / `PLW1514` (unspecified-encoding), Pylint `W1514`, flake8-bugbear
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Context Manager for File and Resource Handling
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Not using with statement for file/network/lock resources causes resource leaks when exceptions are raised, leading to open file handles, locked databases, and memory exhaustion.
|
|
5
|
+
tags: python, resources, context-manager, files, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use Context Manager for File and Resource Handling
|
|
9
|
+
|
|
10
|
+
Python's `with` statement (context manager protocol) guarantees that resources are released even when exceptions occur. Always use `with` when dealing with files, network connections, database cursors, threading locks, and any object that implements `__enter__`/`__exit__`.
|
|
11
|
+
|
|
12
|
+
**Incorrect:**
|
|
13
|
+
```python
|
|
14
|
+
# File handle not closed if exception occurs
|
|
15
|
+
f = open("data.txt", encoding="utf-8")
|
|
16
|
+
content = f.read() # If this raises, f.close() is never called
|
|
17
|
+
f.close()
|
|
18
|
+
|
|
19
|
+
# Lock never released if exception raised inside
|
|
20
|
+
lock = threading.Lock()
|
|
21
|
+
lock.acquire()
|
|
22
|
+
do_work() # Exception here leaves lock permanently acquired
|
|
23
|
+
lock.release()
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Correct:**
|
|
27
|
+
```python
|
|
28
|
+
# File is always closed, even on exception
|
|
29
|
+
with open("data.txt", encoding="utf-8") as f:
|
|
30
|
+
content = f.read()
|
|
31
|
+
|
|
32
|
+
# Lock always released
|
|
33
|
+
import threading
|
|
34
|
+
|
|
35
|
+
lock = threading.Lock()
|
|
36
|
+
with lock:
|
|
37
|
+
do_work()
|
|
38
|
+
|
|
39
|
+
# Multiple context managers in one with
|
|
40
|
+
with open("input.txt", encoding="utf-8") as fin, \
|
|
41
|
+
open("output.txt", "w", encoding="utf-8") as fout:
|
|
42
|
+
fout.write(fin.read())
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**contextlib.suppress as context manager:**
|
|
46
|
+
```python
|
|
47
|
+
import contextlib
|
|
48
|
+
|
|
49
|
+
# Instead of try/except/pass
|
|
50
|
+
with contextlib.suppress(FileNotFoundError):
|
|
51
|
+
os.remove("temp.txt")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Tools:** Ruff `SIM115` (open-file-with-context-handler), `R1732` (consider-using-with), Pylint, flake8-simplify
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Never Use Bare except Clause
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: A bare except catches SystemExit and KeyboardInterrupt, making programs impossible to interrupt and hiding unrelated errors that should propagate.
|
|
5
|
+
tags: python, exceptions, error-handling, pitfalls, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Never Use Bare except Clause
|
|
9
|
+
|
|
10
|
+
A bare `except:` clause (with no exception type specified) catches **every** exception, including `SystemExit` (raised by `sys.exit()`), `KeyboardInterrupt` (Ctrl+C), and `GeneratorExit`. This prevents programs from being terminated normally, swallows programming errors, and makes debugging extremely difficult.
|
|
11
|
+
|
|
12
|
+
Always specify the exception type(s) you intend to handle.
|
|
13
|
+
|
|
14
|
+
**Incorrect:**
|
|
15
|
+
```python
|
|
16
|
+
# Catches SystemExit and KeyboardInterrupt — program cannot be stopped
|
|
17
|
+
try:
|
|
18
|
+
process_data()
|
|
19
|
+
except:
|
|
20
|
+
print("Something went wrong")
|
|
21
|
+
|
|
22
|
+
# Also problematic: too broad
|
|
23
|
+
try:
|
|
24
|
+
connect_to_db()
|
|
25
|
+
except Exception:
|
|
26
|
+
pass # silently ignores all errors including programming mistakes
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Correct:**
|
|
30
|
+
```python
|
|
31
|
+
# Catch only what you expect and can handle
|
|
32
|
+
try:
|
|
33
|
+
process_data()
|
|
34
|
+
except ValueError as e:
|
|
35
|
+
logger.warning("Invalid data: %s", e)
|
|
36
|
+
except OSError as e:
|
|
37
|
+
logger.error("I/O error during processing: %s", e)
|
|
38
|
+
|
|
39
|
+
# If you need a catch-all, at least log it and re-raise
|
|
40
|
+
try:
|
|
41
|
+
connect_to_db()
|
|
42
|
+
except Exception as e:
|
|
43
|
+
logger.error("Unexpected error connecting to DB: %s", e, exc_info=True)
|
|
44
|
+
raise
|
|
45
|
+
|
|
46
|
+
# Correct way to suppress a specific expected error
|
|
47
|
+
import contextlib
|
|
48
|
+
|
|
49
|
+
with contextlib.suppress(FileNotFoundError):
|
|
50
|
+
os.remove("temp.txt")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Exception hierarchy to catch:**
|
|
54
|
+
```python
|
|
55
|
+
# Prefer specific → broad order
|
|
56
|
+
try:
|
|
57
|
+
result = int(user_input)
|
|
58
|
+
except ValueError:
|
|
59
|
+
result = 0 # handle invalid literal
|
|
60
|
+
except (TypeError, OverflowError) as e:
|
|
61
|
+
logger.warning("Type issue: %s", e)
|
|
62
|
+
result = 0
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Tools:** Ruff `E722` (bare-except), `W0702` in Pylint, `BLE001` (blind-except), flake8, pyflakes
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use isinstance() Instead of type() for Type Checking
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Using type() == for type checks breaks polymorphism and inheritance, rejecting valid subclass instances that should be accepted by the contract.
|
|
5
|
+
tags: python, typing, isinstance, quality, oop
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use isinstance() Instead of type() for Type Checking
|
|
9
|
+
|
|
10
|
+
The `type(x) == SomeClass` pattern performs an exact type match and does not account for subclasses. This violates the Liskov Substitution Principle: code that accepts a `list` should also accept `UserList` or any other list subclass.
|
|
11
|
+
|
|
12
|
+
Use `isinstance()` which checks the full MRO (method resolution order) and correctly handles subclasses and abstract base classes.
|
|
13
|
+
|
|
14
|
+
**Incorrect:**
|
|
15
|
+
```python
|
|
16
|
+
def process(data):
|
|
17
|
+
if type(data) == list: # rejects OrderedList, UserList, etc.
|
|
18
|
+
for item in data:
|
|
19
|
+
handle(item)
|
|
20
|
+
if type(data) == dict: # rejects defaultdict, OrderedDict, etc.
|
|
21
|
+
process_mapping(data)
|
|
22
|
+
|
|
23
|
+
def serialize(value):
|
|
24
|
+
if type(value) == str: # rejects str subclasses
|
|
25
|
+
return value.encode("utf-8")
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Correct:**
|
|
29
|
+
```python
|
|
30
|
+
def process(data):
|
|
31
|
+
if isinstance(data, list): # accepts all list subclasses
|
|
32
|
+
for item in data:
|
|
33
|
+
handle(item)
|
|
34
|
+
if isinstance(data, dict): # accepts defaultdict, OrderedDict, etc.
|
|
35
|
+
process_mapping(data)
|
|
36
|
+
|
|
37
|
+
def serialize(value):
|
|
38
|
+
if isinstance(value, str):
|
|
39
|
+
return value.encode("utf-8")
|
|
40
|
+
|
|
41
|
+
# Use abstract base classes for duck typing
|
|
42
|
+
from collections.abc import Mapping, Sequence
|
|
43
|
+
|
|
44
|
+
def process_generic(data):
|
|
45
|
+
if isinstance(data, Sequence): # list, tuple, str, UserList, etc.
|
|
46
|
+
for item in data:
|
|
47
|
+
handle(item)
|
|
48
|
+
if isinstance(data, Mapping): # dict, defaultdict, ChainMap, etc.
|
|
49
|
+
process_mapping(data)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Exception — when exact type match IS needed:**
|
|
53
|
+
```python
|
|
54
|
+
# Only use type() == when you explicitly want to exclude subclasses
|
|
55
|
+
# e.g., in a serialization library distinguishing bool from int:
|
|
56
|
+
if type(value) is bool: # True/False, not int subclasses
|
|
57
|
+
return str(value).lower()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Tools:** Ruff `E721` (type-comparison), Pylint `C0123` (unidiomatic-typecheck), mypy, pyright
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Always Use Timezone-Aware Datetimes
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Naive datetimes (without tzinfo) cause silent bugs when comparing times across timezones, leading to incorrect scheduling, expiry calculations, and audit logs.
|
|
5
|
+
tags: python, datetime, timezone, bugs, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Always Use Timezone-Aware Datetimes
|
|
9
|
+
|
|
10
|
+
Python's `datetime` objects can be *naive* (no timezone) or *aware* (with timezone). Mixing naive and aware datetimes raises a `TypeError`, but naive datetimes used consistently silently produce wrong results when code runs in different timezones (CI server vs production, container vs host).
|
|
11
|
+
|
|
12
|
+
Always create timezone-aware datetimes by passing `tz` or `tzinfo`.
|
|
13
|
+
|
|
14
|
+
**Incorrect:**
|
|
15
|
+
```python
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
|
|
18
|
+
# Naive datetimes — "now" means different things in Tokyo vs London
|
|
19
|
+
created_at = datetime.now()
|
|
20
|
+
expires_at = datetime.utcnow() # UTC but naive — still dangerous
|
|
21
|
+
|
|
22
|
+
# Comparing naive datetimes assumes both are in same timezone (often wrong)
|
|
23
|
+
if datetime.now() > token_expiry: # fails if token_expiry is aware
|
|
24
|
+
raise TokenExpiredError()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Correct:**
|
|
28
|
+
```python
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
|
|
31
|
+
# Python 3.11+: use datetime.UTC constant
|
|
32
|
+
now = datetime.now(tz=timezone.utc)
|
|
33
|
+
|
|
34
|
+
# Python 3.9+: use zoneinfo for local zones
|
|
35
|
+
from zoneinfo import ZoneInfo
|
|
36
|
+
|
|
37
|
+
jst_now = datetime.now(tz=ZoneInfo("Asia/Tokyo"))
|
|
38
|
+
utc_now = datetime.now(tz=timezone.utc)
|
|
39
|
+
|
|
40
|
+
# django/pytz style (pre-3.9)
|
|
41
|
+
import pytz
|
|
42
|
+
|
|
43
|
+
utc_now = datetime.now(tz=pytz.utc)
|
|
44
|
+
|
|
45
|
+
# Always compare aware with aware
|
|
46
|
+
token_expiry: datetime # must be aware
|
|
47
|
+
if datetime.now(tz=timezone.utc) > token_expiry:
|
|
48
|
+
raise TokenExpiredError()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Converting naive to aware (migration):**
|
|
52
|
+
```python
|
|
53
|
+
# Assume a legacy naive datetime is UTC — localize it explicitly
|
|
54
|
+
naive_dt = datetime(2024, 1, 15, 10, 30)
|
|
55
|
+
aware_dt = naive_dt.replace(tzinfo=timezone.utc)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Tools:** Ruff `DTZ001`–`DTZ012` (flake8-datetimez), Pylint `W1502`, mypy with `--strict-optional`
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use pathlib Instead of os.path for File Operations
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: os.path functions return plain strings that are easy to misuse; pathlib.Path provides an object-oriented, composable, and cross-platform API that eliminates common path manipulation bugs.
|
|
5
|
+
tags: python, pathlib, os-path, files, quality, python3
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use pathlib Instead of os.path for File Operations
|
|
9
|
+
|
|
10
|
+
Python 3.4 introduced `pathlib.Path` as a modern, object-oriented replacement for `os.path`. Paths are represented as objects supporting `/` operator for joining, `.stem`, `.suffix`, `.parent` for decomposition, and `.read_text()` / `.write_text()` for content — with no risk of forgetting `os.path.join` and accidentally concatenating strings.
|
|
11
|
+
|
|
12
|
+
**Incorrect:**
|
|
13
|
+
```python
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
# String concatenation — silent bugs on Windows with backslashes
|
|
17
|
+
config_path = base_dir + "/config/" + "settings.json"
|
|
18
|
+
|
|
19
|
+
# Verbose os.path usage
|
|
20
|
+
import os.path
|
|
21
|
+
|
|
22
|
+
full_path = os.path.join(base_dir, "config", "settings.json")
|
|
23
|
+
file_name = os.path.basename(full_path)
|
|
24
|
+
dir_name = os.path.dirname(full_path)
|
|
25
|
+
stem = os.path.splitext(file_name)[0]
|
|
26
|
+
ext = os.path.splitext(file_name)[1]
|
|
27
|
+
|
|
28
|
+
if os.path.exists(config_path):
|
|
29
|
+
with open(config_path, encoding="utf-8") as f:
|
|
30
|
+
content = f.read()
|
|
31
|
+
|
|
32
|
+
os.makedirs(os.path.join(base_dir, "logs"), exist_ok=True)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Correct:**
|
|
36
|
+
```python
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
base_dir = Path("/app")
|
|
40
|
+
|
|
41
|
+
# / operator for joining — cross-platform and clear
|
|
42
|
+
config_path = base_dir / "config" / "settings.json"
|
|
43
|
+
|
|
44
|
+
# Readable decomposition
|
|
45
|
+
file_name = config_path.name # "settings.json"
|
|
46
|
+
dir_name = config_path.parent # Path("/app/config")
|
|
47
|
+
stem = config_path.stem # "settings"
|
|
48
|
+
ext = config_path.suffix # ".json"
|
|
49
|
+
|
|
50
|
+
# Built-in existence check and file reading
|
|
51
|
+
if config_path.exists():
|
|
52
|
+
content = config_path.read_text(encoding="utf-8")
|
|
53
|
+
|
|
54
|
+
# Directory creation
|
|
55
|
+
(base_dir / "logs").mkdir(parents=True, exist_ok=True)
|
|
56
|
+
|
|
57
|
+
# Glob for files
|
|
58
|
+
for py_file in base_dir.rglob("*.py"):
|
|
59
|
+
print(py_file)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Tools:** Ruff `PTH100`–`PTH208` (flake8-use-pathlib), pyupgrade, SonarQube Python rules
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Never Use Wildcard Imports
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Wildcard imports pollute the namespace, make it impossible to trace where a name comes from, and can silently override existing names, causing hard-to-debug subtle bugs.
|
|
5
|
+
tags: python, imports, namespace, quality, readability
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Never Use Wildcard Imports
|
|
9
|
+
|
|
10
|
+
`from module import *` imports every public name from a module into the current namespace. This:
|
|
11
|
+
- Makes it impossible to know where any name is defined without reading the imported module
|
|
12
|
+
- Can silently override names from prior imports or `builtins`
|
|
13
|
+
- Prevents static analysis tools from detecting undefined names
|
|
14
|
+
- Breaks auto-complete and go-to-definition in IDEs
|
|
15
|
+
|
|
16
|
+
The only acceptable use of `import *` is in a package's `__init__.py` to re-export a curated public API defined in `__all__`.
|
|
17
|
+
|
|
18
|
+
**Incorrect:**
|
|
19
|
+
```python
|
|
20
|
+
from os.path import * # which names are now in scope?
|
|
21
|
+
from numpy import * # overrides Python's built-in sum, any, all, etc.
|
|
22
|
+
from models import * # User? Order? both? neither?
|
|
23
|
+
from utils import *
|
|
24
|
+
|
|
25
|
+
# Now these silently shadow built-ins:
|
|
26
|
+
result = sum([1, 2, 3]) # which sum? Python's or numpy's?
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Correct:**
|
|
30
|
+
```python
|
|
31
|
+
import os
|
|
32
|
+
from os import path as osp # or use pathlib
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
import numpy as np # conventional alias
|
|
35
|
+
from models import User, Order, Product # explicit names
|
|
36
|
+
from utils import format_date, validate_email
|
|
37
|
+
|
|
38
|
+
# Clear provenance for every name
|
|
39
|
+
result = np.sum([1, 2, 3])
|
|
40
|
+
full_path = Path("/data") / "file.txt"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Acceptable use in `__init__.py`:**
|
|
44
|
+
```python
|
|
45
|
+
# package/__init__.py — re-export public API
|
|
46
|
+
from .client import Client
|
|
47
|
+
from .exceptions import APIError, RateLimitError
|
|
48
|
+
|
|
49
|
+
__all__ = ["Client", "APIError", "RateLimitError"]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Tools:** Ruff `F403` (undefined-local-with-import-star), `W0401` in Pylint (wildcard-import), flake8, isort
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Lazy Formatting in Logging Calls
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Eager string interpolation in logging (f-strings or %) evaluates and allocates the string even when the log level is disabled, wasting CPU and memory in production.
|
|
5
|
+
tags: python, logging, performance, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use Lazy Formatting in Logging Calls
|
|
9
|
+
|
|
10
|
+
Python's `logging` module accepts a format string and arguments **separately**. The string interpolation only happens if the message will actually be emitted (i.e., the log level is enabled). Using f-strings or `%` interpolation beforehand evaluates the string unconditionally, even if `DEBUG` is disabled.
|
|
11
|
+
|
|
12
|
+
**Incorrect:**
|
|
13
|
+
```python
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
user_id = get_user_id()
|
|
19
|
+
payload = serialize(data)
|
|
20
|
+
|
|
21
|
+
# These always evaluate the f-string / % formatting, even if DEBUG is off
|
|
22
|
+
logger.debug(f"Processing user {user_id} with payload {payload}")
|
|
23
|
+
logger.info("Request from %s: %s" % (ip_address, request.path))
|
|
24
|
+
logger.warning("Slow query: " + str(query_time) + "ms")
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Correct:**
|
|
28
|
+
```python
|
|
29
|
+
import logging
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
# Formatting deferred — only if DEBUG is enabled
|
|
34
|
+
logger.debug("Processing user %s with payload %s", user_id, payload)
|
|
35
|
+
logger.info("Request from %s: %s", ip_address, request.path)
|
|
36
|
+
logger.warning("Slow query: %sms", query_time)
|
|
37
|
+
|
|
38
|
+
# For complex objects, use lazy repr via %r
|
|
39
|
+
logger.debug("Event received: %r", event)
|
|
40
|
+
|
|
41
|
+
# Exception info — use exc_info=True or logger.exception()
|
|
42
|
+
try:
|
|
43
|
+
call_external_api()
|
|
44
|
+
except requests.RequestException as e:
|
|
45
|
+
logger.error("External API failed for user %s: %s", user_id, e, exc_info=True)
|
|
46
|
+
# or equivalently inside an except block:
|
|
47
|
+
# logger.exception("External API failed for user %s", user_id)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Tools:** Ruff `G004` (logging-f-string), `W1201` in Pylint (logging-not-lazy), `W1202` (logging-format-interpolation), `TRY400` (error-instead-of-exception), flake8-logging-format
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use raise...from to Chain Exceptions
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Rethrowing an exception without raise...from loses the original traceback, making it impossible to diagnose the root cause in production logs.
|
|
5
|
+
tags: python, exceptions, error-handling, traceability, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use raise...from to Chain Exceptions
|
|
9
|
+
|
|
10
|
+
When catching an exception and raising a different (or more specific) one, use `raise NewException("msg") from original_exception` to preserve the full causal chain. Without `from`, Python 3 still shows an implicit chain, but the context can be confusing. Using `from` makes the chain explicit and intentional.
|
|
11
|
+
|
|
12
|
+
Using `raise ... from None` deliberately suppresses the original context when it adds no value to the end user.
|
|
13
|
+
|
|
14
|
+
**Incorrect:**
|
|
15
|
+
```python
|
|
16
|
+
import json
|
|
17
|
+
|
|
18
|
+
def load_config(path: str) -> dict:
|
|
19
|
+
try:
|
|
20
|
+
with open(path, encoding="utf-8") as f:
|
|
21
|
+
return json.load(f)
|
|
22
|
+
except json.JSONDecodeError:
|
|
23
|
+
raise ValueError("Config file is malformed") # original traceback lost
|
|
24
|
+
|
|
25
|
+
def get_user(user_id: int) -> dict:
|
|
26
|
+
try:
|
|
27
|
+
return db.find_by_id(user_id)
|
|
28
|
+
except DatabaseError:
|
|
29
|
+
raise RuntimeError("Failed to fetch user") # loses DB error context
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Correct:**
|
|
33
|
+
```python
|
|
34
|
+
import json
|
|
35
|
+
|
|
36
|
+
def load_config(path: str) -> dict:
|
|
37
|
+
try:
|
|
38
|
+
with open(path, encoding="utf-8") as f:
|
|
39
|
+
return json.load(f)
|
|
40
|
+
except json.JSONDecodeError as e:
|
|
41
|
+
raise ValueError(f"Config file '{path}' is malformed") from e # chain preserved
|
|
42
|
+
|
|
43
|
+
def get_user(user_id: int) -> dict:
|
|
44
|
+
try:
|
|
45
|
+
return db.find_by_id(user_id)
|
|
46
|
+
except DatabaseError as e:
|
|
47
|
+
raise UserNotFoundError(f"User {user_id} could not be fetched") from e
|
|
48
|
+
|
|
49
|
+
# Suppress context intentionally (e.g., to hide internal DB details from callers)
|
|
50
|
+
def validate_token(token: str) -> None:
|
|
51
|
+
try:
|
|
52
|
+
jwt.decode(token, SECRET)
|
|
53
|
+
except jwt.ExpiredSignatureError:
|
|
54
|
+
raise AuthenticationError("Token has expired") from None
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Tools:** Ruff `W0707` / `raise-missing-from`, `TRY200` (reraise-no-cause), `B904` (raise-without-from-inside-except), Pylint `W0707`, flake8-bugbear
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Pass Explicit check= to subprocess.run
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: subprocess.run silently ignores non-zero exit codes by default; a failed command goes undetected and subsequent code runs on corrupt or missing output.
|
|
5
|
+
tags: python, subprocess, error-handling, quality, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Pass Explicit check= to subprocess.run
|
|
9
|
+
|
|
10
|
+
`subprocess.run()` has `check=False` by default, meaning a command that exits with a non-zero status (indicating failure) does not raise an exception. Code that follows assumes success and may operate on missing or corrupt output without any error ever being raised.
|
|
11
|
+
|
|
12
|
+
Always pass `check=True` unless you explicitly intend to handle failures yourself by inspecting `returncode`.
|
|
13
|
+
|
|
14
|
+
**Incorrect:**
|
|
15
|
+
```python
|
|
16
|
+
import subprocess
|
|
17
|
+
|
|
18
|
+
# Exit code ignored — if ffmpeg fails, output file doesn't exist
|
|
19
|
+
subprocess.run(["ffmpeg", "-i", "input.mp4", "output.mp4"])
|
|
20
|
+
|
|
21
|
+
# result.returncode is never checked
|
|
22
|
+
result = subprocess.run(["git", "pull"])
|
|
23
|
+
print("Done") # runs even if git pull failed
|
|
24
|
+
|
|
25
|
+
# capture_output=True but still no check
|
|
26
|
+
result = subprocess.run(
|
|
27
|
+
["python", "migrate.py"],
|
|
28
|
+
capture_output=True,
|
|
29
|
+
text=True,
|
|
30
|
+
)
|
|
31
|
+
apply_migrations() # runs even if migrate.py failed
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Correct:**
|
|
35
|
+
```python
|
|
36
|
+
import subprocess
|
|
37
|
+
|
|
38
|
+
# Raises CalledProcessError if command fails
|
|
39
|
+
subprocess.run(
|
|
40
|
+
["ffmpeg", "-i", "input.mp4", "output.mp4"],
|
|
41
|
+
check=True,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Or capture output and check explicitly
|
|
45
|
+
result = subprocess.run(
|
|
46
|
+
["python", "migrate.py"],
|
|
47
|
+
capture_output=True,
|
|
48
|
+
text=True,
|
|
49
|
+
check=True, # raises on non-zero exit code
|
|
50
|
+
)
|
|
51
|
+
apply_migrations() # only reached if migration succeeded
|
|
52
|
+
|
|
53
|
+
# When you WANT to handle failures yourself — explicit intent:
|
|
54
|
+
result = subprocess.run(["git", "pull"], check=False)
|
|
55
|
+
if result.returncode != 0:
|
|
56
|
+
logger.warning("git pull failed with code %d", result.returncode)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Tools:** Ruff `PLW1510` (subprocess-run-without-check), Pylint `W1510`, Bandit `S603`, flake8-bugbear
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Always Set timeout for HTTP Requests
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: HTTP requests without a timeout can hang indefinitely, exhausting thread pools and connection pools, causing cascading failures and outages in production services.
|
|
5
|
+
tags: python, requests, http, reliability, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Always Set timeout for HTTP Requests
|
|
9
|
+
|
|
10
|
+
The `requests` library and similar HTTP clients have **no default timeout**. A single slow or unresponsive server can cause a thread to block indefinitely, exhausting thread pools and causing the entire service to become unresponsive. Always set an explicit `timeout` on every outbound HTTP call.
|
|
11
|
+
|
|
12
|
+
**Incorrect:**
|
|
13
|
+
```python
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
# Hangs forever if server is unresponsive
|
|
17
|
+
response = requests.get("https://api.example.com/data")
|
|
18
|
+
|
|
19
|
+
# Also no timeout
|
|
20
|
+
session = requests.Session()
|
|
21
|
+
response = session.post(
|
|
22
|
+
"https://api.example.com/webhook",
|
|
23
|
+
json=payload,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# httpx — same risk
|
|
27
|
+
import httpx
|
|
28
|
+
response = httpx.get("https://api.example.com/items")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Correct:**
|
|
32
|
+
```python
|
|
33
|
+
import requests
|
|
34
|
+
|
|
35
|
+
# Set both connect and read timeouts as a tuple
|
|
36
|
+
response = requests.get(
|
|
37
|
+
"https://api.example.com/data",
|
|
38
|
+
timeout=(3.05, 27), # (connect_timeout, read_timeout) in seconds
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Or a single value for both
|
|
42
|
+
response = requests.post(
|
|
43
|
+
"https://api.example.com/webhook",
|
|
44
|
+
json=payload,
|
|
45
|
+
timeout=10, # applies to both connect and read
|
|
46
|
+
)
|
|
47
|
+
response.raise_for_status()
|
|
48
|
+
|
|
49
|
+
# httpx — also requires explicit timeout
|
|
50
|
+
import httpx
|
|
51
|
+
|
|
52
|
+
with httpx.Client(timeout=httpx.Timeout(connect=3.0, read=30.0)) as client:
|
|
53
|
+
response = client.get("https://api.example.com/items")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Configure timeouts at the session level:**
|
|
57
|
+
```python
|
|
58
|
+
# Apply uniformly to all requests from the session
|
|
59
|
+
session = requests.Session()
|
|
60
|
+
adapter = requests.adapters.HTTPAdapter(max_retries=3)
|
|
61
|
+
session.mount("https://", adapter)
|
|
62
|
+
|
|
63
|
+
DEFAULT_TIMEOUT = (3.05, 27)
|
|
64
|
+
|
|
65
|
+
def get_with_timeout(url: str, **kwargs) -> requests.Response:
|
|
66
|
+
kwargs.setdefault("timeout", DEFAULT_TIMEOUT)
|
|
67
|
+
return session.get(url, **kwargs)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Tools:** Ruff `W3101` (missing-timeout), Pylint `W3101`, Bandit `S113`, flake8
|