How To
Jul 23, 2024

How to Isolate a Python Module with Tach

How to Isolate a Python Module with Tach

One of the core use cases for Tach is isolating a module such that you can't create dependencies for it. You often want to do this for a variety of reasons:

  1. The module is not meant to have any knowledge of other parts of the codebase. Examples include utils, integrations, etc.
  2. You want to move the module out of the current application, into a microservice.
  3. You want to deprecate the module, and cut out its dependencies.

This is easy to set up and enforce with Tach!

Let’s use the Django library as an example. We’ll enforce isolation of the crypto utils module. This is important, because the crypto module is responsible for password hashing, salting, secure random string generation, timing attack prevention, and more.

If someone unintentionally created a new dependency for the crypto module, changing that dependency could have massive downstream consequences. By enforcing this at the CI level with Tach, we can ensure that won’t happen.

Step 1 - Install Tach

pip install tach

Step 2 - Define your boundaries

We can do this with tach mod , which will open up an interface to configure Tach:

First, let’s identify the python source root in the project. This is the level at which absolute imports within your project operate at. This defaults to the root of the project, but if it’s different, you can mark it with s in tach mod. In Django, it happens to be the root, so we’re good to go!

This also creates an implicit boundary named <root> which we’ll use to validate dependencies of the module we’re looking to isolate.

Second, let’s identify the module which you wish to isolate. We can navigate to the module with the arrow keys, and mark this module with a boundary using enter.

That’s it for tach mod! Let’s save our configuration with ctrl + s.

Step 3 - Sync our Dependencies

Now that we’ve defined our modules, let’s sync the existing dependency state!

Run tach sync , and let’s see what we’ve got:

Uh oh! We can see that the crypto module does have a dependency on the root, meaning that it imports other first party packages and utilizes them.

Step 4 - Resolve Outstanding Dependencies

Let’s see what’s going on by running tach report django/utils/crypto.py

Here we can see that there are two outstanding dependencies for the crypto module:

  1. django.conf.settings
  2. django.utils.encoding.force_bytes

By inspecting the code, we can see that both of these usages are reasonable. We have three options:

  1. Add these modules to our config with tach mod and re-sync, allowing crypto to depend on them.
  2. Explicitly mark these individual imports as allowed, allowing us to fully isolate the remaining dependency set.
  3. Refactor the code to remove the dependencies.

Each approach has its own tradeoffs, but in this case let’s go for #2. This will prevent further usages of the modules being introduced.

In django/utils/crypto.py:

The # tach-ignore directive will tell Tach that these two imports, and only these two lines, are acceptable boundary violations.

Step 5 - Full Isolation

Now that we’ve marked the explicit exceptions, we can fully isolate our crypto module!

Let’s run tach sync one more time and check out our tach.yml config:

Success! The depends_on key for django.utils.crypto is empty. We can see that django.utils.crypto has no first party dependencies (except for the ones we explicitly marked), and the <root> does use the crypto module as intended.

We can check this is working by trying to import anything else into the crypto module, and running tach check:

tach check failing on a newly added dependency

Step 6 - CI Integration

The last step is to add tach check to your CI pipeline. This will cause Tach to explicitly fail on any dependencies that are introduced that don’t map to your tach.yml. It may also be useful to set a CODEOWNER on the tach.yml file, to ensure that your configuration persists correctly.

This also shifts left architecture concerns for your entire development team, and makes your architecture easier to understand for new hires.

Tach also enables you to visualize your dependency graph, enforce boundaries between modules, as well as define strict interfaces for a given module. If there’s a feature you’d like to see in Tach, feel free to drop an issue or join the discussion on Discord!

If you liked this blog post, check out our deep dive on performance optimization with Rust!