python-obfuscator
A Python source-code obfuscator built on the standard-library ast module. It applies multiple independent techniques — each individually togglable — to make code harder to read while keeping it fully executable. See Known limitations before use.
If this project is useful to you, consider sponsoring development.
Installing
pip install python-obfuscator
Requires Python ≥ 3.10.
From source (contributors)
pip install -e ".[dev]"
Or with Poetry:
poetry install
(poetry install pulls dev dependencies from the lockfile; use poetry run <command> to run tools inside that environment.)
Quick start — CLI
# Writes obfuscated/your_file.py (path structure is preserved)
pyobfuscate -i your_file.py
# Print to stdout
pyobfuscate -i your_file.py --stdout
# Disable specific techniques
pyobfuscate -i your_file.py --disable dead_code_injector --disable exec_wrapper
# Show version
pyobfuscate --version
Python API
One-shot helper
from python_obfuscator import obfuscate
source = "x = 1\nprint(x + 2)"
result = obfuscate(source)
print(result)
Selective techniques
from python_obfuscator import obfuscate, ObfuscationConfig
# All techniques except dead-code injection
config = ObfuscationConfig.all_enabled().without("dead_code_injector")
result = obfuscate(source, config=config)
# Only string encoding
config = ObfuscationConfig.only("string_hex_encoder")
result = obfuscate(source, config=config)
Reusing across multiple files (caches the pipeline)
from python_obfuscator import Obfuscator, ObfuscationConfig
obf = Obfuscator(ObfuscationConfig.all_enabled())
for path in my_files:
path.write_text(obf.obfuscate(path.read_text()))
Config combinators
cfg = ObfuscationConfig.all_enabled() # every registered technique
cfg = ObfuscationConfig.only("variable_renamer", "exec_wrapper")
cfg = cfg.without("exec_wrapper") # returns a new frozen config
cfg = cfg.with_added("dead_code_injector")
Techniques
| Name | Priority | What it does |
|---|---|---|
variable_renamer |
10 | Renames local variables, function names, parameters, and class names to visually ambiguous identifiers (lIIllIlI…). Excludes builtins, imports, dunders, and attribute-accessed names. |
string_hex_encoder |
20 | Replaces every string literal "hi" with bytes.fromhex('6869').decode('utf-8'). Skips f-strings. |
dead_code_injector |
30 | Injects dead variable assignments at every scope level — module body, function bodies, class bodies, if/for/while/try/with branches. Some assignments reference other dead variables to simulate computation. |
exec_wrapper |
100 | Wraps the entire module in a single exec("…") call, reducing the top-level AST to one statement. Runs last. |
Techniques are applied in priority order (lowest first).
Example
Input
def greet(name):
msg = "Hello, " + name
print(msg)
greet("world")
Output (all techniques enabled — abridged)
exec('def lIlIllI(IIlIlII):\n lIllIlI = bytes.fromhex(\'48656c6c6f2c20\').decode(\'utf-8\') + IIlIlII\n ...\nlIlIllI(bytes.fromhex(\'776f726c64\').decode(\'utf-8\'))')
Performance overhead
Benchmarks run on an Apple M-series machine, 20 iterations each. The test programs cover OOP, algorithms, functional patterns, number theory, and string processing.
Total overhead (all techniques)
| Program | Original | Obfuscated | Overhead |
|---|---|---|---|
algorithms.py |
0.94 ms | 1.98 ms | +112% |
cipher.py |
1.37 ms | 2.67 ms | +95% |
data_structures.py |
0.72 ms | 2.20 ms | +207% |
functional.py |
0.67 ms | 1.71 ms | +155% |
number_theory.py |
1.84 ms | 3.16 ms | +72% |
oop_simulation.py |
0.68 ms | 1.66 ms | +144% |
Per-technique contribution (average across all programs)
| Technique | Avg overhead | Notes |
|---|---|---|
variable_renamer |
~5% | Pure rename — negligible at runtime |
string_hex_encoder |
~12% | bytes.fromhex call per string literal |
dead_code_injector |
~85% | Dominant cost — dead assignments execute every iteration |
exec_wrapper |
~2% | Single extra exec layer |
The dead-code injector's overhead scales with the number of scopes and loop iterations in the original program. Programs with tight inner loops see the most overhead.
Known limitations
- Class method names are not renamed. Attribute-accessed names (
obj.method) cannot be safely renamed without full type-inference, so the renamer conservatively excludes them. - Keyword argument names are not renamed.
fn(key=val)call-site keyword strings are bare AST strings, notNamenodes, and are not updated when a parameter is renamed. - No scope-aware renaming. The same identifier used in two independent function scopes maps to the same obfuscated name (which is semantically correct but less obfuscated than it could be).
- No control-flow obfuscation. Opaque predicates, bogus branches, and integer encoding are not implemented.
Running the test suite
After a dev install (from source):
pytest
Coverage is enforced at ≥ 95% on every CI run.
# With coverage report
coverage run -m pytest && coverage report
# E2E tests with benchmark output
pytest tests/e2e/ -v -s
With Poetry, run the same commands through the project environment, for example poetry run pytest, poetry run coverage run -m pytest, and poetry run pytest tests/e2e/ -v -s.
Authors
David Teather — davidteather
License
MIT — see LICENSE.
Metadata
Release files for python-obfuscator 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| python_obfuscator-0.1.0.tar.gz | 16.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| python_obfuscator-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 36.2 kB
Release files / python_obfuscator-0.1.0.tar.gz
| Download URL | python_obfuscator-0.1.0.tar.gz |
|---|---|
| Size | 16.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
61300b60808781708ffa45d55d9377c0ba548368f7e0528933f094de399925e9
|
|
BLAKE2b-256 checksum How to use checksums |
d3f08a7f1424c929b6361c81ce850cdf3aa0bca5c62af1633425070e9dd00e9e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 3, 2026.
Transparency logRelease files / python_obfuscator-0.1.0-py3-none-any.whl
| Download URL | python_obfuscator-0.1.0-py3-none-any.whl |
|---|---|
| Size | 19.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e1a08766deae301ce81956b2b653aac8aa3681ea67e312c1a161fbda24b1b896
|
|
BLAKE2b-256 checksum How to use checksums |
1ac4043ec475d101a586fa7aa0fb02ba7cecb4bebd083368d9739047c4ed634d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 3, 2026.
Transparency log