Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ and will output source code compatible with the version of the interpreter it is
This means that if you minify code written for Python 3.11 using python-minifier running with Python 3.12,
the minified code may only run with Python 3.12.

## [Unreleased]

### Added
- New transforms to remove dead code. They are all disabled by default, and repeat until there is nothing left to remove:
+ Remove unused imports, enabled with the `--remove-unused-imports` option.
+ Remove assignments to unused variables, enabled with the `--remove-unused-variables` option.
+ Remove unused function and class definitions, enabled with the `--remove-unused-definitions` option.
+ Remove statements that follow a `return`, `raise`, `break` or `continue`, enabled with the `--remove-unreachable` option.

## [3.4.0] - 2026-09-29

### Added
Expand Down
4 changes: 4 additions & 0 deletions docs/source/minification_options/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,8 @@ They can be enabled or disabled through the minify function, or passing options
rename_globals
remove_asserts
remove_debug
remove_unused_imports
remove_unused_variables
remove_unused_definitions
remove_unreachable
prefer_single_line
24 changes: 24 additions & 0 deletions docs/source/minification_options/remove_unreachable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
def check(value):
if value < 0:
raise ValueError(value)
print('never runs')

if value:
return 'set'
else:
return 'unset'

print('never runs')


def first(items):
for item in items:
if item:
return item
print('runs if there are no items')
return None


def generator():
return
yield
38 changes: 38 additions & 0 deletions docs/source/minification_options/remove_unreachable.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
Remove Unreachable
==================

This transform removes statements that follow a statement that always leaves the block it is in.

The statements after a ``return``, ``raise``, ``break`` or ``continue`` in the same block are removed, as they can never
run. The same is true for the statements after an ``if`` statement where every branch ends this way, so it needs an
``else`` clause. A ``try`` statement ends the block if its ``finally`` clause does, or if its body and all of its
``except`` clauses do. A ``with`` statement ends the block if its body does, unless it only does so by raising
an exception, as the context manager may suppress that. ``for`` and ``while`` loops never end the block, as their body
might not run.

The statements are kept if removing them would change the meaning of the program, for example if they contain a ``yield``,
a ``global`` or ``nonlocal`` declaration, or the only assignment to a local variable that is used before it.

If ``eval()``, ``exec()``, ``locals()``, ``globals()``, ``vars()`` are used, or ``from <module> import *`` is used
in the module, nothing is removed.

Removing code can leave other code unused, so this transform is run again with the other dead code transforms until
there is nothing left to remove. For example the names that were only used by unreachable statements may be removed
by :doc:`remove_unused_imports`.

The transform is disabled by default. Enable it by passing the ``remove_unreachable=True`` argument to the :func:`python_minifier.minify` function,
or passing ``--remove-unreachable`` to the pyminify command.

Example
-------

Input
~~~~~

.. literalinclude:: remove_unreachable.py

Output
~~~~~~

.. literalinclude:: remove_unreachable.min.py
:language: python
25 changes: 25 additions & 0 deletions docs/source/minification_options/remove_unused_definitions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
def helper():
return 1


def unused():
return helper()


class Unused:
def method(self):
return 1


@register
def registered():
pass


def main():
def inner():
pass
return 2


print(main())
47 changes: 47 additions & 0 deletions docs/source/minification_options/remove_unused_definitions.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
Remove Unused Definitions
=========================

This transform removes functions and classes that are never used.

A function or class definition is removed if its name is not used anywhere in the module. Methods and the other names
defined in a class body are never removed, only functions and classes defined at module level or inside a function.

A definition is kept if executing it could have an effect, which is when:

- It has a decorator, as the decorator may register the function or class somewhere
- A default value, annotation, base class or keyword evaluates something that isn't known to be harmless, like a call
- The body of a class runs anything that isn't a simple assignment, a docstring or a definition

This is not always safe, so the transform is disabled by default. It could break any program that imports a
definition from the minified module, or looks up definitions by a string name. A class without a decorator could
still be registered by a base class or metaclass.

Definitions are never removed:

- If ``eval()``, ``exec()``, ``locals()``, ``globals()``, ``vars()`` are used, or ``from <module> import *`` is used
in the module
- If the name is a dunder name like ``__getattr__``
- If the name is included as a literal string in ``__all__`` at module level, or ``__all__`` is used in a way that
can't be determined by looking at the module

If a definition is removed and a statement is still required, it is replaced by a zero expression statement.

Removing code can leave other code unused, so this transform is run again with the other dead code transforms until
there is nothing left to remove. For example removing a function also removes any helper that only that function used.

Enable this source transformation by passing the ``remove_unused_definitions=True`` argument to the :func:`python_minifier.minify`
function, or passing ``--remove-unused-definitions`` to the pyminify command.

Example
-------

Input
~~~~~

.. literalinclude:: remove_unused_definitions.py

Output
~~~~~~

.. literalinclude:: remove_unused_definitions.min.py
:language: python
12 changes: 12 additions & 0 deletions docs/source/minification_options/remove_unused_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import os
import sys, json
from collections import OrderedDict, defaultdict
from os import path as os_path

print(sys.argv)
print(defaultdict(list))


def listing():
import glob
return []
46 changes: 46 additions & 0 deletions docs/source/minification_options/remove_unused_imports.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
Remove Unused Imports
=====================

This transform removes imports of names that are never used.

An import statement is removed if none of the names it binds are used. If only some of the names are unused, just those
names are removed from the statement. A name is used if it is read anywhere in the module, including in nested
functions, decorators and default arguments.

This is not always safe, so the transform is disabled by default. It could break:

- Any program that imports a name from the minified module, as nothing is known about how the module is used elsewhere
- Imports that are only used for their side effects, like registering a codec
- Names that are only referred to by strings, for example type annotations that are string literals or code run by ``getattr()``

Imports are never removed:

- If ``eval()``, ``exec()``, ``locals()``, ``globals()``, ``vars()`` are used, or ``from <module> import *`` is used
in the module
- If they are ``__future__`` imports
- If the name is included as a literal string in ``__all__`` at module level, or ``__all__`` is used in a way that
can't be determined by looking at the module
- If they are in a class body, where the names become attributes of the class

If an import statement is removed and a statement is still required, it is replaced by a zero expression statement.

Removing code can leave other code unused, so this transform is run again with the other dead code transforms until
there is nothing left to remove. For example if :doc:`remove_unused_definitions` removes a function, the imports that
were only used by that function are removed too.

Enable this source transformation by passing the ``remove_unused_imports=True`` argument to the :func:`python_minifier.minify`
function, or passing ``--remove-unused-imports`` to the pyminify command.

Example
-------

Input
~~~~~

.. literalinclude:: remove_unused_imports.py

Output
~~~~~~

.. literalinclude:: remove_unused_imports.min.py
:language: python
18 changes: 18 additions & 0 deletions docs/source/minification_options/remove_unused_variables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
TIMEOUT = 30
RETRIES = TIMEOUT * 2
VERSION = '1.0'
__version__ = VERSION

started = start()


def process(items):
total = 0
scratch = [item for item in items]
lookup = {'a': 1}
for item in items:
total += item
return total


print(RETRIES)
48 changes: 48 additions & 0 deletions docs/source/minification_options/remove_unused_variables.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
Remove Unused Variables
=======================

This transform removes assignments to variables that are never used.

An assignment is removed if the name is not used anywhere in the module, and evaluating the assigned value is known not
to have any effect. Assigning a literal, a collection of literals, a name, an attribute, a subscript, a lambda,
or an expression made of operators with these is removed. Anything that calls a function, uses ``await`` or ``yield``,
or is a comprehension is kept, as is any value that might unpack an iterable or mapping.

Only assignments that bind a single name are removed, including annotated assignments with a value.
Assignments to attributes and subscripts, to multiple targets, and augmented assignments are always kept.
Using a name with ``del``, an augmented assignment or ``global`` and ``nonlocal`` declarations all count as a use.

This is not always safe, so the transform is disabled by default. It could break any program that imports a
variable from the minified module, or reads variables by a string name. Reading an attribute or subscript, and applying
an operator, are assumed to have no effect, but they may run code that does.

Assignments are never removed:

- If ``eval()``, ``exec()``, ``locals()``, ``globals()``, ``vars()`` are used, or ``from <module> import *`` is used
in the module
- If the name is a dunder name like ``__version__``
- If the name is included as a literal string in ``__all__`` at module level, or ``__all__`` is used in a way that
can't be determined by looking at the module
- If they are in a class body, where the names become attributes of the class

If an assignment is removed and a statement is still required, it is replaced by a zero expression statement.

Removing code can leave other code unused, so this transform is run again with the other dead code transforms until
there is nothing left to remove.

Enable this source transformation by passing the ``remove_unused_variables=True`` argument to the :func:`python_minifier.minify`
function, or passing ``--remove-unused-variables`` to the pyminify command.

Example
-------

Input
~~~~~

.. literalinclude:: remove_unused_variables.py

Output
~~~~~~

.. literalinclude:: remove_unused_variables.min.py
:language: python
20 changes: 19 additions & 1 deletion src/python_minifier/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
)
from python_minifier.transforms.combine_imports import CombineImports
from python_minifier.transforms.constant_folding import FoldConstants
from python_minifier.transforms.dead_code import eliminate_dead_code
from python_minifier.transforms.remove_annotations import RemoveAnnotations
from python_minifier.transforms.remove_annotations_options import RemoveAnnotationsOptions
from python_minifier.transforms.remove_asserts import RemoveAsserts
Expand Down Expand Up @@ -75,7 +76,11 @@ def minify(
remove_builtin_exception_brackets=True,
constant_folding=True,
prefer_single_line=False,
remove_dead_branches=True
remove_dead_branches=True,
remove_unused_imports=False,
remove_unused_variables=False,
remove_unused_definitions=False,
remove_unreachable=False
):
"""
Minify a python module
Expand Down Expand Up @@ -112,6 +117,10 @@ def minify(
:param bool constant_folding: If literal expressions should be evaluated
:param bool prefer_single_line: If semi-colons should be preferred over newlines where there is no difference in output size
:param bool remove_dead_branches: If if-statements with a constant False test should be removed
:param bool remove_unused_imports: If imported names that are never used should be removed
:param bool remove_unused_variables: If assignments to variables that are never used should be removed
:param bool remove_unused_definitions: If functions and classes that are never used should be removed
:param bool remove_unreachable: If statements that follow a return, raise, break or continue should be removed

:rtype: str

Expand Down Expand Up @@ -164,6 +173,15 @@ def minify(
if remove_dead_branches:
module = RemoveDeadBranches()(module)

if remove_unused_imports or remove_unused_variables or remove_unused_definitions or remove_unreachable:
module = eliminate_dead_code(
module,
remove_imports=remove_unused_imports,
remove_variables=remove_unused_variables,
remove_definitions=remove_unused_definitions,
remove_unreachable=remove_unreachable
)

if remove_explicit_return_none:
module = RemoveExplicitReturnNone()(module)

Expand Down
6 changes: 5 additions & 1 deletion src/python_minifier/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ def minify(
remove_builtin_exception_brackets: bool = ...,
constant_folding: bool = ...,
prefer_single_line: bool = ...,
remove_dead_branches: bool = ...
remove_dead_branches: bool = ...,
remove_unused_imports: bool = ...,
remove_unused_variables: bool = ...,
remove_unused_definitions: bool = ...,
remove_unreachable: bool = ...
) -> Text: ...


Expand Down
28 changes: 28 additions & 0 deletions src/python_minifier/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,30 @@ def parse_args():
help='Disable removing branches that are never executed',
dest='remove_dead_branches',
)
minification_options.add_argument(
'--remove-unused-imports',
action='store_true',
help='Enable removing imports of names that are never used',
dest='remove_unused_imports',
)
minification_options.add_argument(
'--remove-unused-variables',
action='store_true',
help='Enable removing assignments to variables that are never used',
dest='remove_unused_variables',
)
minification_options.add_argument(
'--remove-unused-definitions',
action='store_true',
help='Enable removing functions and classes that are never used',
dest='remove_unused_definitions',
)
minification_options.add_argument(
'--remove-unreachable',
action='store_true',
help='Enable removing statements that follow a return, raise, break or continue',
dest='remove_unreachable',
)

annotation_options = parser.add_argument_group('remove annotations options', 'Options that affect how annotations are removed')
annotation_options.add_argument(
Expand Down Expand Up @@ -391,6 +415,10 @@ def do_minify(source, filename, minification_args):
remove_builtin_exception_brackets=minification_args.remove_exception_brackets,
constant_folding=minification_args.constant_folding,
prefer_single_line=minification_args.prefer_single_line,
remove_unused_imports=minification_args.remove_unused_imports,
remove_unused_variables=minification_args.remove_unused_variables,
remove_unused_definitions=minification_args.remove_unused_definitions,
remove_unreachable=minification_args.remove_unreachable,
)

# Encode minified result to bytes for comparison and output
Expand Down
Loading