diff --git a/requirements.txt b/requirements.txt
index 40fe650..04c8918 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,6 +4,7 @@ pyflakes
 coverage
 sphinx
 pytest-cov
+nudatus>=0.0.2 
 
 # Mock is bundled as part of unittest since Python 3.3
 # mock_open can't read binary data in <= 3.4.2
diff --git a/uflash.py b/uflash.py
index 7e1872f..4d0d0b3 100644
--- a/uflash.py
+++ b/uflash.py
@@ -21,6 +21,13 @@ import sys
 from subprocess import check_output
 import time
 
+# nudatus is an optional dependancy
+can_minify = True
+try:
+    import nudatus
+except ImportError:  # pragma: no cover
+    can_minify = False
+
 #: The magic start address in flash memory for a Python script.
 _SCRIPT_ADDR = 0x3e000
 
@@ -41,6 +48,7 @@ Documentation is here: https://uflash.readthedocs.io/en/latest/
 
 #: MAJOR, MINOR, RELEASE, STATUS [alpha, beta, final], VERSION
 _VERSION = (1, 1, 1, )
+_MAX_SIZE = 8188
 
 
 def get_version():
@@ -50,6 +58,15 @@ def get_version():
     return '.'.join([str(i) for i in _VERSION])
 
 
+def get_minifier():
+    """
+    Report the minifier will be used when minify=True
+    """
+    if can_minify:
+        return 'nudatus'
+    return None
+
+
 def strfunc(raw):
     """
     Compatibility for 2 & 3 str()
@@ -57,7 +74,7 @@ def strfunc(raw):
     return str(raw) if sys.version_info[0] == 2 else str(raw, 'utf-8')
 
 
-def hexlify(script):
+def hexlify(script, minify=False):
     """
     Takes the byte content of a Python script and returns a hex encoded
     version of it.
@@ -69,11 +86,15 @@ def hexlify(script):
     # Convert line endings in case the file was created on Windows.
     script = script.replace(b'\r\n', b'\n')
     script = script.replace(b'\r', b'\n')
+    if minify:
+        if not can_minify:
+            raise ValueError("No minifier is available")
+        script = nudatus.mangle(script.decode('utf-8')).encode('utf-8')
     # Add header, pad to multiple of 16 bytes.
     data = b'MP' + struct.pack('<H', len(script)) + script
     # Padding with null bytes in a 2/3 compatible way
     data = data + (b'\x00' * (16 - len(data) % 16))
-    if len(data) > 8192:
+    if len(data) > _MAX_SIZE:
         # 'MP' = 2 bytes, script length is another 2 bytes.
         raise ValueError("Python script must be less than 8188 bytes.")
     # Convert to .hex format.
@@ -252,7 +273,7 @@ def save_hex(hex_file, path):
 
 
 def flash(path_to_python=None, paths_to_microbits=None,
-          path_to_runtime=None, python_script=None):
+          path_to_runtime=None, python_script=None, minify=False):
     """
     Given a path to or source of a Python file will attempt to create a hex
     file and then flash it onto the referenced BBC micro:bit.
@@ -285,9 +306,9 @@ def flash(path_to_python=None, paths_to_microbits=None,
         if not path_to_python.endswith('.py'):
             raise ValueError('Python files must end in ".py".')
         with open(path_to_python, 'rb') as python_script:
-            python_hex = hexlify(python_script.read())
+            python_hex = hexlify(python_script.read(), minify)
     elif python_script:
-        python_hex = hexlify(python_script)
+        python_hex = hexlify(python_script, minify)
 
     runtime = _RUNTIME
     # Load the hex for the runtime.
@@ -374,6 +395,9 @@ def main(argv=None):
     parser.add_argument('-w', '--watch',
                         action='store_true',
                         help='Watch the source file for changes.')
+    parser.add_argument('-m', '--minify',
+                        action='store_true',
+                        help='Minify the source')
     parser.add_argument('--version', action='version',
                         version='%(prog)s ' + get_version())
     args = parser.parse_args(argv)
@@ -402,7 +426,7 @@ def main(argv=None):
     else:
         try:
             flash(path_to_python=args.source, paths_to_microbits=args.target,
-                  path_to_runtime=args.runtime)
+                  path_to_runtime=args.runtime, minify=args.minify)
         except Exception as ex:
             error_message = (
                 "Error flashing {source} to {target}{runtime}: {error!s}"

Allow for script minification
There should be a flag (`--minify` and `-m`) that causes uflash to "minify" the Python script using various heuristics so it'll fit into the limited amount of memory available.

See: https://github.com/mu-editor/mu/pull/385 for an example of what I mean.
diff --git a/tests/test_uflash.py b/tests/test_uflash.py
index d3746a3..38c561f 100644
--- a/tests/test_uflash.py
+++ b/tests/test_uflash.py
@@ -44,6 +44,17 @@ def test_get_version():
     assert result == '.'.join([str(i) for i in uflash._VERSION])
 
 
+def test_get_minifier():
+    """
+    When a minifier was loaded a string identifing it should be
+    returned, otherwise None
+    """
+    with mock.patch('uflash.can_minify', False):
+        assert uflash.get_minifier() is None
+    with mock.patch('uflash.can_minify', True):
+        assert len(uflash.get_minifier()) > 0
+
+
 def test_hexlify():
     """
     Ensure we get the expected .hex encoded result from a "good" call to the
@@ -383,7 +394,7 @@ def test_flash_with_python_script():
         with mock.patch('uflash.find_microbit', return_value='bar'):
             with mock.patch('uflash.hexlify') as mock_hexlify:
                 uflash.flash(python_script=python_script)
-                mock_hexlify.assert_called_once_with(python_script)
+                mock_hexlify.assert_called_once_with(python_script, False)
 
 
 def test_flash_cannot_find_microbit():
@@ -409,6 +420,25 @@ def test_flash_wrong_python():
             assert 'Will only run on Python ' in ex.value.args[0]
 
 
+def test_hexlify_minify_without_minifier():
+    """
+    When minification but no minifier is available a ValueError
+    should be raised
+    """
+    with pytest.raises(ValueError):
+        with mock.patch('uflash.can_minify', False):
+            uflash.hexlify(TEST_SCRIPT, minify=True)
+
+
+def test_hexlify_minify():
+    """
+    Check mangle is called as expected
+    """
+    with mock.patch('nudatus.mangle') as mangle:
+        uflash.hexlify(TEST_SCRIPT, minify=True)
+        mangle.assert_called_once_with(TEST_SCRIPT.decode('utf-8'))
+
+
 def test_main_no_args():
     """
     If there are no args into the main function, it simply calls flash with
@@ -419,7 +449,8 @@ def test_main_no_args():
             uflash.main()
             mock_flash.assert_called_once_with(path_to_python=None,
                                                paths_to_microbits=[],
-                                               path_to_runtime=None)
+                                               path_to_runtime=None,
+                                               minify=False)
 
 
 def test_main_first_arg_python():
@@ -431,7 +462,8 @@ def test_main_first_arg_python():
         uflash.main(argv=['foo.py'])
         mock_flash.assert_called_once_with(path_to_python='foo.py',
                                            paths_to_microbits=[],
-                                           path_to_runtime=None)
+                                           path_to_runtime=None,
+                                           minify=False)
 
 
 def test_main_first_arg_help(capsys):
@@ -557,7 +589,8 @@ def test_main_two_args():
         mock_flash.assert_called_once_with(
             path_to_python='foo.py',
             paths_to_microbits=['/media/foo/bar'],
-            path_to_runtime=None)
+            path_to_runtime=None,
+            minify=False)
 
 
 def test_main_multiple_microbits():
@@ -572,7 +605,8 @@ def test_main_multiple_microbits():
             path_to_python='foo.py',
             paths_to_microbits=[
                 '/media/foo/bar', '/media/foo/baz', '/media/foo/bob'],
-            path_to_runtime=None)
+            path_to_runtime=None,
+            minify=False)
 
 
 def test_main_runtime():
@@ -585,7 +619,8 @@ def test_main_runtime():
         mock_flash.assert_called_once_with(
             path_to_python='foo.py',
             paths_to_microbits=['/media/foo/bar'],
-            path_to_runtime='baz.hex')
+            path_to_runtime='baz.hex',
+            minify=False)
 
 
 def test_main_named_args():
@@ -596,7 +631,8 @@ def test_main_named_args():
         uflash.main(argv=['-r', 'baz.hex'])
         mock_flash.assert_called_once_with(path_to_python=None,
                                            paths_to_microbits=[],
-                                           path_to_runtime='baz.hex')
+                                           path_to_runtime='baz.hex',
+                                           minify=False)
 
 
 def test_main_watch_flag():

pip install -e .[dev]
parse_log_pytest
requirements.txt
numpy>=1.16.0 pandas>=1.0.0 nudatus pytest
apt-get update apt-get install -y gcc
3.9
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
exceptiongroup==1.2.2
iniconfig==2.1.0
nudatus==0.0.5
numpy==2.0.2
packaging==24.2
pandas==2.2.3
pluggy==1.5.0
pytest==8.3.5
python-dateutil==2.9.0.post0
pytz==2025.2
six==1.17.0
tomli==2.2.1
tzdata==2025.2
-e git+https://github.com/ntoll/uflash.git@8139fe11aa97c45334592cd8546ebb306ac41ccb#egg=uflash

name: uflash
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - exceptiongroup==1.2.2
      - iniconfig==2.1.0
      - nudatus==0.0.5
      - numpy==2.0.2
      - packaging==24.2
      - pandas==2.2.3
      - pluggy==1.5.0
      - pytest==8.3.5
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - six==1.17.0
      - tomli==2.2.1
      - tzdata==2025.2
prefix: /opt/conda/envs/uflash

tests/test_uflash.py::test_get_minifier tests/test_uflash.py::test_flash_with_python_script tests/test_uflash.py::test_hexlify_minify_without_minifier tests/test_uflash.py::test_hexlify_minify tests/test_uflash.py::test_main_no_args tests/test_uflash.py::test_main_first_arg_python tests/test_uflash.py::test_main_two_args tests/test_uflash.py::test_main_multiple_microbits tests/test_uflash.py::test_main_runtime tests/test_uflash.py::test_main_named_args
tests/test_uflash.py::test_get_version tests/test_uflash.py::test_hexlify tests/test_uflash.py::test_unhexlify tests/test_uflash.py::test_unhexlify_not_python tests/test_uflash.py::test_unhexlify_bad_unicode tests/test_uflash.py::test_hexlify_empty_script tests/test_uflash.py::test_embed_hex tests/test_uflash.py::test_embed_no_python tests/test_uflash.py::test_embed_no_runtime tests/test_uflash.py::test_extract tests/test_uflash.py::test_extract_sandwiched tests/test_uflash.py::test_extract_not_valid_hex tests/test_uflash.py::test_extract_no_python tests/test_uflash.py::test_find_microbit_posix_exists tests/test_uflash.py::test_find_microbit_posix_missing tests/test_uflash.py::test_find_microbit_nt_exists tests/test_uflash.py::test_find_microbit_nt_missing tests/test_uflash.py::test_find_microbit_unknown_os tests/test_uflash.py::test_save_hex tests/test_uflash.py::test_save_hex_no_hex tests/test_uflash.py::test_save_hex_path_not_to_hex_file tests/test_uflash.py::test_flash_no_args tests/test_uflash.py::test_flash_has_python_no_path_to_microbit tests/test_uflash.py::test_flash_with_path_to_multiple_microbits tests/test_uflash.py::test_flash_with_path_to_microbit tests/test_uflash.py::test_flash_with_path_to_runtime tests/test_uflash.py::test_flash_cannot_find_microbit tests/test_uflash.py::test_flash_wrong_python tests/test_uflash.py::test_main_first_arg_help tests/test_uflash.py::test_main_first_arg_version tests/test_uflash.py::test_main_first_arg_not_python tests/test_uflash.py::test_flash_raises tests/test_uflash.py::test_flash_raises_with_info tests/test_uflash.py::test_watch_raises tests/test_uflash.py::test_extract_raises tests/test_uflash.py::test_main_watch_flag tests/test_uflash.py::test_extract_command tests/test_uflash.py::test_extract_paths tests/test_uflash.py::test_extract_command_source_only tests/test_uflash.py::test_extract_command_no_source tests/test_uflash.py::test_watch_no_source tests/test_uflash.py::test_watch_file tests/test_uflash.py::test_hexlify_validates_script_length
requirements.txt uflash.py
requirements.txt uflash.py