<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alex Strick van Linschoten</title>
    <description>The latest articles on DEV Community by Alex Strick van Linschoten (@alexzenml).</description>
    <link>https://dev.to/alexzenml</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F749580%2F3ec69467-11c2-4dce-97ca-db380c09ee3d.jpeg</url>
      <title>DEV Community: Alex Strick van Linschoten</title>
      <link>https://dev.to/alexzenml</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alexzenml"/>
    <language>en</language>
    <item>
      <title>How to improve your experimentation workflows with MLflow Tracking and ZenML</title>
      <dc:creator>Alex Strick van Linschoten</dc:creator>
      <pubDate>Thu, 24 Feb 2022 15:41:46 +0000</pubDate>
      <link>https://dev.to/zenml/how-to-improve-your-experimentation-workflows-with-mlflow-tracking-and-zenml-2dhe</link>
      <guid>https://dev.to/zenml/how-to-improve-your-experimentation-workflows-with-mlflow-tracking-and-zenml-2dhe</guid>
      <description>&lt;p&gt;Most professional or so-called 'citizen' data scientists will be familiar with the scenario that sees you spending a day trying out a dozen different model training configurations in which you experiment with various hyper parameters or perhaps different pre-trained models. As evening falls, you emerge from the haze of experimentation and you ask yourself: which of my experiments offered the best results for the problem I'm trying to solve?&lt;/p&gt;

&lt;p&gt;At this point, especially for smaller use cases or where you were unsure if a hunch was worth pursuing and just wanted to try a few things out, you might be left empty-handed, unable to give an answer one way or another beyond some hunch that there &lt;em&gt;was&lt;/em&gt; one set of parameters that really performed well, if only you could remember what they were… And if someone asked you to reproduce the steps it took you to create a particular model, would you even be able to do that?&lt;/p&gt;

&lt;p&gt;This would be one of those times where it's worth reminding ourselves that data science includes the word 'science', and that we need to be careful around how we track and reason about models. The workflows and practice of machine learning is sufficiently complicated (and often non-deterministic) that we need rigorous ways of ensuring that we really are doing what we think we are doing, and that we can reproduce our work. (It's not for nothing that 'reproducibility' is &lt;a href="https://petewarden.com/2018/03/19/the-machine-learning-reproducibility-crisis/" rel="noopener noreferrer"&gt;often&lt;/a&gt; &lt;a href="https://www.technologyreview.com/2019/02/18/137357/machine-learning-is-contributing-to-a-reproducibility-crisis-within-science/" rel="noopener noreferrer"&gt;paired&lt;/a&gt; with 'crisis'.)&lt;/p&gt;

&lt;p&gt;There are manual ways that you could use to help address this problem, but they're unlikely to be sufficient. Will your spreadsheet experiment tracker really capture &lt;em&gt;everything&lt;/em&gt; you needed to produce a particular model? (Think about how the particular configuration or random split of data is so central to how your model performs.) What you really want is something that will handle all this tracking of data and parameters, in as automatic a way as is possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use MLflow Tracking?
&lt;/h2&gt;

&lt;p&gt;Enter, &lt;a href="https://mlflow.org/docs/latest/tracking.html" rel="noopener noreferrer"&gt;MLflow Tracking&lt;/a&gt;, part of &lt;a href="https://mlflow.org/docs/latest/concepts.html" rel="noopener noreferrer"&gt;a wider ecosystem&lt;/a&gt; of tooling offered by MLflow to help you train robust and reproducible models. Other commonly-used pieces are &lt;a href="https://mlflow.org/docs/latest/model-registry.html" rel="noopener noreferrer"&gt;the model registry&lt;/a&gt; (which stores any model artifacts created during the training process) as well as their flexible suite of plugins and integrations allowing you to &lt;a href="https://mlflow.org/docs/latest/models.html#built-in-deployment-tools" rel="noopener noreferrer"&gt;deploy the models&lt;/a&gt; you create.&lt;/p&gt;

&lt;p&gt;MLflow Tracking is what allows you to track all those little parts of your model training workflow. Not only does it hook into an artifact store of your choosing (such as that offered by ZenML), but it offers a really useful UI interface which you can use to inspect pipeline runs and experiments you conduct. If you want to compare the performance or accuracy of several experiments (i.e. pipeline runs), some diagrams and charts are only a few clicks away. This flexible interface goes a really long way to solving some of the problems mentioned earlier.&lt;/p&gt;

&lt;p&gt;One really useful feature offered by MLflow Tracking is that of &lt;a href="https://mlflow.org/docs/latest/tracking.html#automatic-logging" rel="noopener noreferrer"&gt;automatic logging&lt;/a&gt;. Many commonly-used machine learning libraries (such as &lt;code&gt;scikit-learn&lt;/code&gt;, Pytorch, &lt;code&gt;fastai&lt;/code&gt; and Tensorflow / Keras) support this. You either call &lt;code&gt;mlflow.autolog()&lt;/code&gt; just before your training code, or you use a library-specific version of that (e.g. &lt;code&gt;mlflow.sklearn.autolog()&lt;/code&gt;). In this way, MLflow will handle logging metrics, parameters and models without the need for explicit log statements. (Note that you can also include the &lt;a href="https://mlflow.org/docs/latest/tracking.html#logging-data-to-runs" rel="noopener noreferrer"&gt;non-automated logging&lt;/a&gt; of whatever custom properties are important for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  ZenML + MLflow Tracking = 🚀
&lt;/h2&gt;

&lt;p&gt;If you're using ZenML to bring together the various tools in your machine learning stack, you'll probably be eager to use some of this tracking goodness and make your own experiments more robust. ZenML actually &lt;em&gt;already&lt;/em&gt; partly supported what MLflow Tracking does in the sense that any artifacts going in or out of the steps of your ZenML pipeline were being tracked, stored and versioned in your artifact and metadata store. (You're welcome!) But until now we didn't have a great way for you to interact with that metadata about your experiments and pipeline runs that was non-programmatic and also visual.&lt;/p&gt;

&lt;p&gt;MLflow Tracking gives you that ability to inspect the various experiments and pipeline runs in the (local) web interface and is probably going to be a friendlier way of interacting with and reasoning about your machine learning experiments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm695n5ozqsakpbm2vb8u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm695n5ozqsakpbm2vb8u.png" alt="Tracking machine learning training runs with MLFlow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You could have used MLflow Tracking in the past, too, but with our latest integration updates ZenML handles some of the boilerplate complicated setup that comes with using MLflow. There are &lt;a href="https://mlflow.org/docs/latest/tracking.html#where-runs-are-recorded" rel="noopener noreferrer"&gt;different ways&lt;/a&gt; of deploying the tracking infrastructure and servers and it isn't a completely painless task to set all this up and to get going with MLflow Tracking. This is where we make your life a bit easier: we setup everything you need to use it on your (currently: local) machine, connecting the MLFlow Tracking interface to your ZenML artifact store. It can be a bit tricky to configure the relevant connections between the various modular pieces that talk to each other, and we hide this from you beneath an abstraction.&lt;/p&gt;

&lt;p&gt;We think that this ability to converse between the MLflow universe and the ZenML universe is extremely powerful, and this approach is at the heart of what we are trying to build with our tool to help you work with reproducible and robust machine learning pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Just tell me how to use it already!
&lt;/h2&gt;

&lt;p&gt;The best place to see MLflow Tracking and ZenML being used together in a simple use case is &lt;a href="https://github.com/zenml-io/zenml/tree/main/examples/mlflow_tracking" rel="noopener noreferrer"&gt;our example&lt;/a&gt; that showcases the integration. It builds on the quickstart example, but shows how you can add in MLflow to handle the tracking. In order to enable MLflow to track artifacts inside a particular step, all you need is to decorate the step with &lt;code&gt;@enable_mlflow&lt;/code&gt; and then to specify what you want logged within the step. Here you can see how this is employed in a model training step that uses the &lt;code&gt;autolog&lt;/code&gt; feature I mentioned above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Define the step and enable mlflow - order of decorators is important here
&lt;/span&gt;&lt;span class="nd"&gt;@enable_mlflow&lt;/span&gt;
&lt;span class="nd"&gt;@step&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tf_trainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TrainerConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;x_train&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Train a neural net from scratch to recognize MNIST digits return our
    model or the learner&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sequential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Flatten&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_shape&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
            &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optimizers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Adam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;losses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SparseCategoricalCrossentropy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_logits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accuracy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tensorflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;autolog&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;x_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;epochs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;epochs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# write model
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If, for any reason, you need to access the global environment parameters used by ZenML to automatically configure MLflow (which define where and how experiments and runs are displayed and stored in the MLflow Tracking UI/system), we've got you covered. These global parameters can be easily accessed through the &lt;code&gt;Environment&lt;/code&gt; singleton object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;zenml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;integrations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mlflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mlflow_environment&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MLFLOW_ENVIRONMENT_NAME&lt;/span&gt;
&lt;span class="n"&gt;mlflow_env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="n"&gt;MLFLOW_ENVIRONMENT_NAME&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check out &lt;a href="https://apidocs.zenml.io/0.6.1/api_docs/environment/" rel="noopener noreferrer"&gt;the API docs&lt;/a&gt; to learn more about the &lt;code&gt;Environment&lt;/code&gt; object and watch this space for a blog post where we explain more about why we chose to add this recently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Over to you now!
&lt;/h2&gt;

&lt;p&gt;If you're inspired by this illustration of how you can make your machine learning workflow that little bit more reproducible and robust, check out &lt;a href="https://github.com/zenml-io/zenml/tree/main/examples/mlflow_tracking" rel="noopener noreferrer"&gt;the full example&lt;/a&gt; that illustrates the integration. If you use it in your own code base, please do let us know — &lt;a href="https://zenml.io/slack-invite/" rel="noopener noreferrer"&gt;say hi on Slack&lt;/a&gt;! — and as always if you have any questions, we're here for you.&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>machinelearning</category>
      <category>experimentation</category>
      <category>mlops</category>
    </item>
    <item>
      <title>10 Ways To Level Up Your Testing with Python</title>
      <dc:creator>Alex Strick van Linschoten</dc:creator>
      <pubDate>Wed, 10 Nov 2021 12:01:59 +0000</pubDate>
      <link>https://dev.to/alexzenml/10-ways-to-level-up-your-testing-with-python-25ni</link>
      <guid>https://dev.to/alexzenml/10-ways-to-level-up-your-testing-with-python-25ni</guid>
      <description>&lt;p&gt;There's nothing like working on testing to get you familiar with a codebase. I've been &lt;a href="https://github.com/zenml-io/zenml/pull/118"&gt;working&lt;/a&gt; on &lt;a href="https://github.com/zenml-io/zenml/pull/149"&gt;adding back&lt;/a&gt; in &lt;a href="https://github.com/zenml-io/zenml/pull/130"&gt;some testing&lt;/a&gt; to &lt;a href="https://github.com/zenml-io/zenml"&gt;the ZenML codebase&lt;/a&gt; this past couple of weeks and as a relatively new employee here, it has been a really useful way to dive into how things work under the hood.&lt;/p&gt;

&lt;p&gt;This being my first time working seriously with Python, there were a few things that I had to learn along the way. What follows is an initial set of lessons I took away from the experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. One-size-fits-all won't cut it
&lt;/h2&gt;

&lt;p&gt;Looking at things from a higher level, it's important to realise that there are lots of different approaches that you could take to testing. It's a truism that you should 'test intent, not implementation', but I imagine that in some scenarios like for software being deployed on a space shuttle you'd want to maybe also test the implementation as well.&lt;/p&gt;

&lt;p&gt;Similarly, different companies and projects have different needs for testing. If you're a huge company, testing is a way of ensuring reliability and preventing catastrophic failures along the way. If you're a small company, where speed of creation and the pace of development is frantic, having too rigid a set of tests may actually end up hurting you by stifling your ability to iterate through ideas and changes quickly.&lt;/p&gt;

&lt;p&gt;I found it helped to take a step back early on in my testing to really think through what I was doing, why I was doing it, and  what larger goal it was there to support.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. 'Don't be that person': testing to crush the spirits of your team
&lt;/h2&gt;

&lt;p&gt;It's worth reiterating the previous remark about testing intent and not implementation.&lt;/p&gt;

&lt;p&gt;If you test every last conditional statement, checking that the code is built in exactly that specific way, changing anything in the original codebase is going to become incredibly tiresome. Moreover, your testing library will start to resemble a kind of byzantine twin replica of your original code.&lt;/p&gt;

&lt;p&gt;For preventing this, it helps if everyone in the team is testing as much as they are writing new code. This way it is just part of the development process and not a separate add-on from a QA-like team. At ZenML, we're small enough that the expectation is that if you work on a new feature, you should also be responsible for writing the tests that go alongside.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Pytest, O Pytest!
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.pytest.org/en/latest/"&gt;Pytest&lt;/a&gt; is amazing. It has everything you need to write your tests, is easy to understand, and has great documentation of even the slightly more niche features. Can you tell I really enjoyed getting to know this open-source library?&lt;/p&gt;

&lt;p&gt;For now, I'll mention some of the really useful combinations of CLI commands that I found useful.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# make the test output verbose&lt;/span&gt;
pytest tests/ &lt;span class="nt"&gt;-v&lt;/span&gt;

&lt;span class="c"&gt;# stop testing whenever you get to a test that fails&lt;/span&gt;
pytest tests/ &lt;span class="nt"&gt;-x&lt;/span&gt;

&lt;span class="c"&gt;# run only a single test&lt;/span&gt;
pytest tests/test_base.py::test_initialization

&lt;span class="c"&gt;# run only tests tagged with a particular word&lt;/span&gt;
pytest tests/ &lt;span class="nt"&gt;-m&lt;/span&gt; specialword

&lt;span class="c"&gt;# print out all the output of tests to the console&lt;/span&gt;
pytest tests/ &lt;span class="nt"&gt;-s&lt;/span&gt;

&lt;span class="c"&gt;# run all the tests, but run the last failures first&lt;/span&gt;
pytest tests/ &lt;span class="nt"&gt;--ff&lt;/span&gt;

&lt;span class="c"&gt;# see which tests will be run with the given options and config&lt;/span&gt;
pytest tests/ —collect-only

&lt;span class="c"&gt;# show local variables in tracebacks&lt;/span&gt;
pytest tests/ —showlocals
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there are so many more! The flexibility of the CLI tool allows you to be really nimble and ensures you don't have to hang around for already-passing tests to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Temp Files &amp;amp; Temp Directory Choice Paralysis
&lt;/h2&gt;

&lt;p&gt;At a certain point I needed to test that certain functions were having side effects out in the real world of a filesystem. I didn't want to pollute my hard drive or that of whatever random CI server was running the tests, so then I started looking around for options for the creation of temporary files and directories.&lt;/p&gt;

&lt;p&gt;It turns out that between the Python standard library, Pytest and some library-specific features, we're spoiled for choice when it comes for convenience helpers to create temporary files and directories. Python has &lt;a href="https://docs.python.org/3/library/tempfile.html"&gt;&lt;code&gt;tempfile&lt;/code&gt;&lt;/a&gt; which is a platform-agnostic way of creating temporary files and directories. Pytest has &lt;code&gt;tmp_path&lt;/code&gt; which you can insert as an argument into your test function and have a convenience location which you can use to your heart's content. (There are also &lt;a href="https://docs.pytest.org/en/latest/how-to/tmp_path.html#tmp-path"&gt;several other options&lt;/a&gt; with Pytest). Then other libraries you're using may have specific testing capabilities. We use &lt;a href="https://click.palletsprojects.com/en/8.0.x/"&gt;&lt;code&gt;click&lt;/code&gt;&lt;/a&gt; for our CLI functionality and there's a &lt;a href="https://click.palletsprojects.com/en/8.0.x/testing/#file-system-isolation"&gt;useful convenience pattern&lt;/a&gt; for running commands from a temporary directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_something&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
   &lt;span class="n"&gt;runner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CliRunner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
   &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isolated_filesystem&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
      &lt;span class="c1"&gt;# do something here in your new temporary directory
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Decorate your way to clearer test code
&lt;/h2&gt;

&lt;p&gt;Pytest has a bunch of helper functions which enhance the test code you already have. For instance, if you want to wanted to iterate over a series of values and pass them in as arguments to a function, you can just use the &lt;code&gt;parametrize&lt;/code&gt; functionality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test_input,expected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="s"&gt;"3+5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2+4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"6*9"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that this would fail because 6x9 does not equal to 42.&lt;/p&gt;

&lt;p&gt;If you have a test that you know is failing right now, but you want to put it to the side for the moment, you can mark it down as being expected to fail with &lt;code&gt;xfail&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xfail&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_something&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# whatever code you have here doesn't work
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I find it's more useful in this way to get a full sense of which tests aren't working rather than just commenting them out.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;mark&lt;/code&gt; method in general is a great way of creating some custom ways to run your tests. You could — using a &lt;code&gt;@pytest.mark.no_async_call_required&lt;/code&gt; decorator — distinguish between tests that take a bit longer to run and tests that are more or less instantaneous, for example.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Use &lt;code&gt;hypothesis&lt;/code&gt; for random arguments
&lt;/h2&gt;

&lt;p&gt;Hypothesis is a Python library to check that functions work the way you think they do. It works by setting up certain conditions under which the function should work.&lt;/p&gt;

&lt;p&gt;For example, you can say that this function should be able to accept any &lt;code&gt;datetime&lt;/code&gt; value without any problem. Instead of trying to come up with a list of different possible edge cases, hypothesis instead will run (in parallel) a whole series of values to check that this is actually the case. As the docs state:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"It works by generating arbitrary data matching your specification and checking that your guarantee still holds in that case. If it finds an example where it doesn’t, it takes that example and cuts it down to size, simplifying it until it finds a much smaller example that still causes the problem. It then saves that example for later, so that once it has found a problem with your code it will not forget it in the future." (&lt;a href="https://hypothesis.readthedocs.io/en/latest/"&gt;source&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These custom ways of testing certain kinds of inputs are called 'strategies', and it has &lt;a href="https://hypothesis.readthedocs.io/en/latest/data.html#core-strategies"&gt;a whole bunch&lt;/a&gt; of these to choose from. The ones I most often use are text, integers, decimals and &lt;code&gt;datetime&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Use &lt;code&gt;tox&lt;/code&gt; to test multiple versions of Python
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://tox.wiki/en/latest/"&gt;&lt;code&gt;tox&lt;/code&gt;&lt;/a&gt; allows you to automate running your test suite through multiple versions of Python. It's likely that your CI process does this as well, so in order to test that these are passing locally as well, you can use &lt;code&gt;tox&lt;/code&gt;. It creates new virtual environments using the versions you specify and runs your test suite through each of them.&lt;/p&gt;

&lt;p&gt;Note that if you're using &lt;code&gt;pyenv&lt;/code&gt; as your overall Python version manager, you may have to use something like the following command to make sure that all the various Python versions are available to &lt;code&gt;tox&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pyenv &lt;span class="nb"&gt;local &lt;/span&gt;zenml-dev-3.8.6 3.6.9 3.7.11 3.8.11 3.9.6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first argument passed in is my development environment in which I usually work, but the other Python versions / environments are to make those versions available to &lt;code&gt;tox&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Debug your failing tests with &lt;code&gt;pdb&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Pytest has a bunch of handy ways of inspecting exactly what's going on at the point where a test fails. I showed some of those above, where you can show, for example, whatever local variables were initialized alongside the stacktrace.&lt;/p&gt;

&lt;p&gt;Another really useful feature is the &lt;code&gt;--pdb&lt;/code&gt; flag which you can pass in along with your CLI command. This will deposit you inside a &lt;code&gt;pdb&lt;/code&gt; debugging environment at exactly the moment your test fails. Super useful that we get all this convenience functionality out of the box with Pytest.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Linting: before and beyond testing
&lt;/h2&gt;

&lt;p&gt;At ZenML we use &lt;a href="https://pre-commit.com/"&gt;&lt;code&gt;pre-commit&lt;/code&gt;&lt;/a&gt; hooks that kick into action whenever you try to commit code. (Check out &lt;a href="https://github.com/zenml-io/zenml/blob/main/pyproject.toml"&gt;our &lt;code&gt;pyproject.toml&lt;/code&gt; configuration&lt;/a&gt; and &lt;a href="https://github.com/zenml-io/zenml/tree/main/scripts"&gt;our &lt;code&gt;scripts/&lt;/code&gt; directory&lt;/a&gt; to see how we handle this!) It ensures a level of consistency throughout our codebase, ensuring that all &lt;a href="https://interrogate.readthedocs.io/en/latest/index.html"&gt;our functions have docstrings&lt;/a&gt;, for example, or implementing a standard order for &lt;code&gt;import&lt;/code&gt; statements.&lt;/p&gt;

&lt;p&gt;Some of this — the &lt;a href="https://mypy.readthedocs.io/en/stable/index.html"&gt;&lt;code&gt;mypy&lt;/code&gt;&lt;/a&gt; hook, for example — starts to verge into what feels like testing territory. By ensuring that functions all have type annotations you sometimes are doing more than just enforcing a particular coding style. When you add &lt;code&gt;mypy&lt;/code&gt; into your development workflow, you get up close and personal with exactly how different types are passed around in your codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. …and remember, coverage is just a number!
&lt;/h2&gt;

&lt;p&gt;It's always good to have a number to chase. It gives you something to work towards and a feeling of progress. Tools like &lt;a href="https://codecov.io"&gt;Codecov&lt;/a&gt; offer fancy visualizations of just which parts of your codebase still need some attention. Automating all this as part of the CI process can highlight when you've just added a series of features but no accompanying tests.&lt;/p&gt;

&lt;p&gt;Bearing all these positives in mind, you should still always remember that your tests are there to serve your broader goals. If your goal is to rapidly iterate and create new features, maybe having a goal of 100% test coverage at all times is an unrealistic expectation. A 100% test coverage does not necessarily mean your code is bug-free and robust. It just means that you invoked it during the testing process.&lt;/p&gt;

&lt;p&gt;Similarly, different kinds of codebase will have different kinds of test weightings. We didn't really talk much about the different types of tests (from unit to integration to usability), but some systems or types of designs will require more focus on different pieces of this bigger picture.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Alex Strick van Linschoten is a Machine Learning Engineer at ZenML.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>testing</category>
      <category>beginners</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
