Best practices

Recommended repository structure and best practices.

Until now, you probably have kept all your code in one single file. In production, it is recommended to split up your steps and pipelines into separate files.

├── .dockerignore
├── Dockerfile
├── steps
│   ├── loader_step
│   │   ├── .dockerignore (optional)
│   │   ├── Dockerfile (optional)
│   │   ├──
│   │   └── requirements.txt (optional)
│   └── training_step
│       └── ...
├── pipelines
│   ├── training_pipeline
│   │   ├── .dockerignore (optional)
│   │   ├── config.yaml (optional)
│   │   ├── Dockerfile (optional)
│   │   ├──
│   │   └── requirements.txt (optional)
│   └── deployment_pipeline
│       └── ...
├── notebooks
│   └── *.ipynb
├── requirements.txt
├── .zen

Check out how to initialize your project from a template following best practices in the Project templates section.


Keep your steps in separate Python files. This allows you to optionally keep their utils, dependencies, and Dockerfiles separate.


ZenML records the root python logging handler's output into the artifact store as a side-effect of running a step. Therefore, when writing steps, use the logging module to record logs, to ensure that these logs then show up in the ZenML dashboard.

# Use ZenML handler
from zenml.logger import get_logger

logger = get_logger(__name__)

def training_data_loader():
    # This will show up in the dashboard"My logs")


Just like steps, keep your pipelines in separate Python files. This allows you to optionally keep their utils, dependencies, and Dockerfiles separate.

It is recommended that you separate the pipeline execution from the pipeline definition so that importing the pipeline does not immediately run it.

Do not give pipelines or pipeline instances the name "pipeline". Doing this will overwrite the imported pipeline and decorator and lead to failures at later stages if more pipelines are decorated there.

Pipeline names are their unique identifiers, so using the same name for different pipelines will create a mixed history where two versions of a pipeline are two very different entities.


Containerized orchestrators and step operators load your complete project files into a Docker image for execution. To speed up the process and reduce Docker image sizes, exclude all unnecessary files (like data, virtual environments, git repos, etc.) within the .dockerignore.

Dockerfile (optional)

By default, ZenML uses the official zenml docker image as a base for all pipeline and step builds. You can use your own Dockerfile to overwrite this behavior. Learn more here.


Collect all your notebooks in one place.


By running zenml init at the root of your project, you define the project scope for ZenML. In ZenML terms, this will be called your "source's root". This will be used to resolve import paths and store configurations.

Although this is optional, it is recommended that you do this for all of your projects.

All of your import paths should be relative to the source's root.

Putting your pipeline runners in the root of the repository ensures that all imports that are defined relative to the project root resolve for the pipeline runner. In case there is no .zen defined this also defines the implicit source's root.

Last updated