---
title: "Understanding the purpose of tox.ini"
date: 2019-12-18
description: "What the tox tool is, how to configure tox.ini, and why its config file is used by other Python tools like flake8 and pytest."
tags: [python, testing]
---

## Introduction

I was confused by the Python tool [tox](https://tox.readthedocs.io/en/latest/) for a long time, and was unsure what it was really used for considering lots of different Python packages appeared to rely on tox's configuration file (e.g. `tox.ini`).

In this post I'm going to briefly explain what the tox tool is, and what a `tox.ini` configuration file looks like and why that configuration file can be used for more than just serving the purposes of the tox tool.

## What is tox?

Tox is a tool that creates [virtual environments](/posts/python-app-dependencies/#virtual-environments), and installs the configured dependencies for those environments, for the purpose of testing a Python package (i.e. something that will be shared via [PyPi](https://pypi.org/), and so it only works with code that defines a `setup.py`).

> [!INFO]
> PyPi == Python Package Index (i.e. where you _typically_ install all your Python packages from, when executing `pip install`).

The file that you use to configure tox can be one of the following...

- `tox.ini`
- `pyproject.toml` (see [PEP 518](https://www.python.org/dev/peps/pep-0518/))
- `setup.cfg` (see [official guide to distributing packages](https://packaging.python.org/guides/distributing-packages-using-setuptools/))

> [!INFO]
> these ^^ are all [ini file](https://en.wikipedia.org/wiki/INI_file) formats -- which is information that will become more relevant/important later on.

## Example tox.ini

All configuration options for tox can be found here: [tox.readthedocs.io/en/latest/config.html](https://tox.readthedocs.io/en/latest/config.html) but the below example is a simple demonstration of how you might configure tox for a real package (e.g. this is an _actual_ `tox.ini` file I've used).

```ini
[tox]
envlist = 
    py37, lint
toxworkdir = 
    {env:TOX_WORK_DIR}

[testenv]
setenv =
    PYTHONDONTWRITEBYTECODE = 1
whitelist_externals =
    /bin/bash
deps = 
    -rrequirements-dev.txt
commands =
    py.test --cov={envsitepackagesdir}/bf_tornado -m "not integration"

[testenv:dev]
usedevelop=True
recreate = False
commands =
    # to run arbitrary commands: tox -e dev -- bash
    {posargs:py.test --cov=bf_tornado}

[testenv:lint]
deps =
    flake8==3.8.3
    mypy==0.782
commands =
    flake8 bf_tornado
    mypy --verbose --ignore-missing-imports --package bf_tornado
```

> [!INFO]
> the name after `testenv:` is the _name_ of the virtual environment that will be created (e.g. `testenv:foo` will create a "foo" virtual environment).

## Configuring _other_ packages

A `tox.ini` file can be used to configure different types of packages, which is confusing at first because the tox home page suggests that tox is used to test _your own_ packages you plan on distributing to PyPi.

What the tox authors mean by that description, is that the `tox` _command_ itself is used to handle testing your packages, while the `tox.ini` _configuration file_ is just one such file that can be used to contain configuration information.

This is why other packages, such as [Flake8](https://flake8.pycqa.org/en/latest/index.html) allow you to [configure Flake8](https://flake8.pycqa.org/en/latest/user/configuration.html) using the `tox.ini` file, as well as alternatives such as: `setup.cfg` or `.flake8`.

The key to understanding why this works is as follows: each of these files conform to the [ini file](https://en.wikipedia.org/wiki/INI_file) format. So you're free to use whatever file _name_ you feel best suits your project, while the format of the file will stay consistent to what is expected of an `.ini` file.

## Why do multiple tools support `tox.ini`?

The benefit of these various tools supporting the `tox.ini` filename specifically is to help reduce the number of configuration files a project requires.

Imagine you needed a unique configuration file for every Python command line tool you use in your project. How many would that result in? How hard would it be to remember all the various file names?

With that in mind, below is an example that shows various Python packages being configured within a `tox.ini` file.

And in case it's unclear, the configuration inside of the `tox.ini` file is used instead of having to pass those configuration values via the command line.

So in the case of a tool such as `flake8`, instead of using `flake8 --max-line-length=120` you could just call `flake8` and the flag value is extracted from the configuration file.

```ini
[flake8]
max_line_length = 120
ignore = E261,E265,E402  # http://pep8.readthedocs.org/en/latest/intro.html#error-codes

[coverage:run]
branch = True

[coverage:report]
show_missing = True
exclude_lines =
    raise NotImplementedError
    return NotImplemented
    def __repr__
omit = bf_tornado/testing.py

[pytest]
addopts = 
    --strict -p no:cacheprovider --showlocals
markers =
    integration: mark a test as an integration test that makes http calls.
```

## Bonus Section

Although not directly related to the conversation about `tox.ini` I thought it worth mentioning that I discovered recently the Python logging library allows you to be able to configure logging via an ini configuration file!

> [!INFO]
> Reference: [docs.python.org/3/howto/logging.html](https://docs.python.org/3/howto/logging.html#configuring-logging)

The ini file might look something like the following:

```
[loggers]
keys=root,simpleExample

[handlers]
keys=consoleHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)

[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=
```

The Python program that uses this file might look like the following:

```
import logging
import logging.config

logging.config.fileConfig('logging.conf')

# create logger
logger = logging.getLogger('simpleExample')

# 'application' code
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')
```

Notice though, the _name_ of the configuration file we imported was `logging.conf` (no `.ini` extension). It doesn't matter that the file doesn't use the `.ini` extension as long as the format of the file itself conforms to the ini format.
