All You Need to Know to Get Started With Python Poetry

April 30, 2024
Written by
Ana Paula Gomes
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Diane Phan
Twilion

Over the years, Python packaging has undergone many changes and improvements. Today, we have different tools that can be used to solve problems, such as dependency management, packaging, and publishing. In this post, you will see how to implement a Python package using Poetry , a tool that covers all three steps when developing a Python package. You will walk through the process of creating a new project, converting it into a package, installing dependencies, and publishing it to PyPI . By the end of this post, you will have a good understanding of how to use the Poetry tools to simplify the Python packaging process.

Prerequisites

For this you’ll need:

  • Python 3.8+ (all OS are welcome)
  • Poetry 1.4.2
  • GitHub account (for the GitHub Actions steps)

Yes, that is it. If you’re using pip, you only need to run pip install poetry==1.4.2. Other options for installation are available here .

Create a new project with Poetry

In the command line, create a directory named " random-poems", and navigate inside it. Then, run the command poetry init to initialize a new Poetry project. This command will guide you through the process of creating your project's configuration file (pyproject.toml).

During the initialization process, Poetry will ask you questions related to your project, such as the name of your package, its version, and its author. You can answer or skip these questions by pressing Enter to use the default values.

After you have provided the necessary information, Poetry will prompt you to add dependencies to your project. First, it will ask you about production dependencies, which are packages that your project requires to function correctly. You can add these dependencies by specifying their names and versions or skip this step by typing No. Poetry will then ask you about test dependencies, which are packages that your project requires to run tests.

Once you have finished adding dependencies, Poetry will show you the entire configuration file before generating it. This allows you to review and confirm that everything is correct. If you are satisfied with the configuration file, type Yes to proceed, and Poetry will generate it for you.

Here's an example of what the configuration file might look like:

[tool.poetry]
name = "random-poems"
version = "0.1.0"
description = "A collection of randomly generated poems"
authors = ["Your Name <your@email.com>"]
packages = [{include = "random_poems"}]

[tool.poetry.dependencies]
python = "^3.10"

This is an excellent description of your new Python project! However, you'll need to activate your virtual environment to maintain dependency isolation. Simply run poetry shell.

Create a Python package

In line 8 of your code, you will see that the package random_poems is included in Poetry. This means that when you install this package using Poetry, the expected Python module will also be automatically installed.

Create a new Python module called "random_poems". Create a folder called random_poems and add a file called __init__.py inside it.

Once you have created this module, you can run the poetry install command to install the package. This will ensure that both the package and its associated Python module are installed correctly and ready to use in your project. You should see something like this:

Installing dependencies from lock file

No dependencies to install or update

Installing the current project: random-poems (0.1.0)

After installation, a new file called poetry.lock is created that contains a list of all installed packages and their respective versions.

Add some code in the package

Now you have a package! Yay! But you still need to add some code to it.

In the random_poemsmodule, create a package called poems.py. Add the method declaim there:

def declaim(title, poem, author):
    print("---------------------------------------------------")
    print(f"{title}\n")
    print(poem)
    print(f"\nBy {author}")
    print("---------------------------------------------------")

It will just print a poem whenever you pass one. Now you only need to pass a poem… Wait, I know a nice place to get random poems from public-domain sources! The website Poetrydb can provide the poems to you via API. How cool is that? You only need to make a request for it using the requests library. To add it to your project, you just need to run poetry add requests.

Below, you can see the method that your app will execute to call a random poem:

def get_random_poem():
    response = requests.get("https://poetrydb.org/random")
    response.raise_for_status()
    poem = response.json()[0]
    return dict(
        title=poem['title'],
        poem="\n".join(poem['lines']),
        author=poem['author']
    )

The get_random_poem method will make a request to https://poetrydb.org/random and get its first result. The payload returns the poem’s text in lines, so when returning a dictionary, these lines are put together, separated by a break line \n.

You can play around with your Python REPL to see whether both methods work. Hit python in your package’s terminal and import both methods from poems.py file:

from random_poems.poems import declaim, get_random_poem

poem = get_random_poem()
declaim(poem['title'], poem['poem'], poem['author'])

You should see a lovely poem in your terminal. Once you are done, you can leave the terminal hitting Control + D. Coming back to the code above, can you add this to a CLI? That’s what you will do in the next step.

If you're looking for a more streamlined approach, Poetry's new command can create the module and test folders for you, saving you time and effort. It will create the directory, the project file, the package and the tests folder for you. Example: The command poetry new another-random-poems-demo creates a new folder named "another-random-poems-demo".

Add a CLI to your package

This CLI (Command Line Interface) receives an argument and then gets a poem and declaims it. Create a cli.py inside random_poemswith the following content:

import argparse

from random_poems.poems import get_random_poem, declaim

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('declaim', help='Declaim a random poem')
    args = parser.parse_args()

    if args.declaim:
        poem = get_random_poem()
        declaim(poem['title'], poem['poem'], poem['author'])

Using Python’s CLI argparse you will receive the argument declaim when the user wants to see a poem on the screen. If the argument exists, you will call the methods developed before to get a random poem and declaim it.

The if __name__ == "__main__": construct in Python is a common idiom used in scripts and modules to determine whether the Python script is being run as the main program or if it is being imported as a module into another script.

You can run it using python random_poems/cli.py declaim. But the idea is to get Poetry to give you an excellent interface. Go to pyproject.toml and add the following section to the end of the file:

[tool.poetry.scripts]
random_poems = "random_poems.cli:main"

In this section, you are giving a name to your CLI executable and showing its path: the Python modules and the method that will be called. After that, install your package poetry install and run:

random_poems declaim

Simple and sweet!

Add new dependencies and tests

To explore the structure of a project with Poetry more, you will add a testsdirectory in the root of your project repository. It should look like this:

.git
.github
.gitignore     
.venv          
dist
poetry.lock    
random_poems
tests
README.md      
pyproject.toml

Before you start writing tests, you should add pytest as a dependency. Remember that it is a good practice to separate development dependencies from production dependencies. Poetry helps you with that by offering the flag --group as an option. Install pytest using the command poetry add --group dev pytest to see it in action. This command will add pytest as a dependency in the group dev. Later on, if you want to configure a CI to run tests or deploy the application, installing it altogether or specific to the group is possible.

Pytest is set. It's time to write some tests! In the tests directory, create the file test_poems.py. There, you will add the following test as an example:

from random_poems.poems import declaim

class TestDeclaim:
    def test_declaim(self, capsys):
        title = "Super Cool"
        poem = (
            "Roses are red\n"
            "Violets are blue\n"
            "I think the Global South\n"
            "is super cool"
        )
        author = "Tonino"
        expected_output = (
            "---------------------------------------------------\n"
            "Super Cool\n"
            "\n"
            "Roses are red\n"
            "Violets are blue\n"
            "I think the Global South\n"
            "is super cool\n"
            "\n"
            "By Tonino\n"
            "---------------------------------------------------\n"
        )

        declaim(title, poem, author)

        captured = capsys.readouterr()

        assert captured.out == expected_output

Pytest will recognize all classes that start with "Test" as test class and methods/functions that start with test_ test methods and execute them. In the test class TestDeclaim, there is only one test method test_declaim, that will check if the poem was declaimed correctly - that means if it was shown correctly in the stdout. Pytest offers a fixture called capsys to capture the output. You only need to receive it as an argument in the test declaration. After that, you create all the necessary things to declaim a poem (having a title, a poem, and an author). You should also create a variable with the expected output to compare with the output brought by the method declaim. After calling declaim, you use capsys to read the output. Then, it is time to compare the captured and expected output.

Run pytest to execute all tests. Another alternative to run a command (in this case, pytest) is poetry run pytest. The tests should pass! Feel free to play around with testing other methods.

Publish your Poetry package

At this point, you have your CLI up and running. This is exciting! Poetry also does the heavy lifting for you to publish it. You can configure your credentials directly from the CLI and run poetry publish.

Run poetry build and see the magic happening. ✨This is the expected output:

Building random-poems (0.1.0)
  - Building sdist
  - Built random_poems-0.1.0.tar.gz
  - Building wheel
  - Built random_poems-0.1.0-py3-none-any.whl

In order to publish a project, you will need to have an account in PyPI . You can register there; I’ll wait. Don’t forget to enable the two-factor authentication for more security as well.

After registering, go to Publishing and fill in the name of your package (if you feel like publishing a package; if not, you can skip this step) and access all projects. Once you’re done, PyPI will give you a token. Be sure to copy and store it in a safe place.

Return to your project’s terminal and run the command below with the token you just received. You only need to do this once for each new project added.

poetry config pypi-token.pypi <pypi-token-here>

After that, you can run poetry publish and the magic will happen. In the background, poetry will upload the wheel to PyPI and make your package available. That’s it!

A few notes about this process: when running the command poetry publish, Poetry assumes that you want to publish to PyPI by default. If you want to publish to another repository, you must inform which repository using the flag --repository. You can also pass a username and password as arguments.

You can see the package of this project available here .

Understand the Poetry Package and GitHub Actions

You just published to PyPI, so let’s move to CI/CD tools and use GitHub Actions for it. In this section, two essential recipes to have your library up and running on GitHub Actions: testing and releasing.

Test the application

Create a new file .github/workflows/tests.yml and place the content below there. This job will run on every push. It installs Poetry 1.4.2 and Python 3.10 with Poetry’s cache for more speed. Then, it installs the dependencies and runs the tests. Exactly the same commands you used during this tutorial!

name: Tests

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Install Poetry
      run: |
        pip install --upgrade pip
        pip install poetry==1.4.2
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.10'
        cache: 'poetry'
    - name: Install dependencies
      run: poetry install
    - name: Run tests
      run: poetry run pytest -v

Publish to GitHub Releases

You learned to publish a Python wheel from your local environment to PyPI. Another alternative is publishing your package to GitHub releases. This is quite useful, especially for cases where you have a private package and need it to be available only internally.

Before you jump into the yml file, you need to know how to bump a new version of your Poetry package. This is one of the parts I like the most :) You need to run  the command poetry version. Poetry follows the SemVer convention so that you will have major to prerelease options. See poetry version --help for more details. Once you run this command, it will change the file pyproject.toml with the latest version. You still need to commit to this change, but that’s it.

Now that you know how to bump your package’s version, you can follow the release recipe. First, create the file .github/workflows/release.yml and place the following content there. The job below will be triggered on two occasions

  • 1. when the workflow is triggered in the GitHub Actions Interface (there, you can choose the bump type in a small form) 
  • 2. when a new tag is created.

    Once Python and Poetry are configured, you will bump the version and store it in the variable CURRENT_VERSION because you will use it later in the commit message. You will also need to commit to the new version from the CI. Once done, the Action ncipollo/release-action will generate a new release with the artifacts from dist/.
name: Release

on:
  workflow_dispatch:
    inputs:
      version:
        type: choice
        required: true
        description: "Version bump type"
        options:
        - patch
        - minor
        - major
  push:
    tags:
      - '*.*.*'

jobs:
  release:
    name: Release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python 3.10
        uses: actions/setup-python@v5
        with:
          python-version: "3.10"

      - name: Install Poetry
        run: |
          pip install --upgrade pip
          pip install poetry==1.4.2

      - name: Update PATH
        run: echo "$HOME/.local/bin" >> $GITHUB_PATH

      - name: Bump Version
        run: |
          poetry version "${{ github.event.inputs.version }}"
          CURRENT_VERSION=$(poetry version --short)
          echo "CURRENT_VERSION=${CURRENT_VERSION}" >> $GITHUB_ENV

      - name: Fail if the current version doesn't exist
        if: env.CURRENT_VERSION == ''
        run: exit 1

      - name: Build project for distribution
        run: poetry build

      - name: Commit new version
        run: |
          git config --global user.name "Your Repo CI"
          git config --global user.email "YourRepoCI@users.noreply.github.com"
          git commit -a -m "Bump version to $CURRENT_VERSION"
          git push origin main

      - name: Create Release
        uses: ncipollo/release-action@v1
        with:
          artifacts: "dist/*.whl"
          token: ${{ secrets.GITHUB_TOKEN }}
          draft: false
          generateReleaseNotes: true
          tag: ${{ env.CURRENT_VERSION }}
          commit: main

What's next for the Poetry Python package?

You are not a Poetry beginner anymore! But I thought you might have a few questions, so here is a short Q&A for you:

  • Can I use it in an existing project? Yes, you can! poetry init will do the job. Then you can gradually migrate the steps you might have in your workflow.
  • Can I generate a requirements.txt out of it? Yes! Just run poetry export > requirements.txt, and there you go! See poetry export --help for more options.
  • How can I use Poetry with different Python versions? Yes! You can point to the path where the version is installed like poetry env use python3.7 and also with popular tools like pyenv. See more info about it here.

I use Poetry in all my projects because the API works beautifully. It is as simple and practical as Python is meant to be. I hope you got excited about it, too, and can try it at least! If you want to keep playing around with this project, I have a few suggestions. You can install the test library responses and test the method get_random_poem. Also, try to publish your own library to PyPI (or to Test PyPI !). If you want to go one step further, replace the step of releasing GitHub Releases to publishing to PyPI. What do you think?

Explore more of it in Poetry’s docs . The code for this tutorial is working and available for you in this repository . Have fun!

Ana Paula Gomes is a senior software engineer who has worked with Python for a decade, transitioning to the academic world and different companies. She’s currently doing a Ph.D. in Artificial Intelligence for Public Health. You can follow her work here https://github.com/anapaulagomes and here www.anapaulagomes.me .