---
title: Strict Typing in Python
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Overview

This guide explains how to use Python's type system effectively with the Infrahub SDK, focusing on the use of Protocols for type-safe development.

:::note What is Python Typing

Python typing allows you to specify the expected data types of variables, function arguments, and return values to improve code clarity and catch bugs early.

```python
# Basic type hints
def percentage(num1: int, num2: int) -> float:
    return (num1 / num2) * 100
```

:::

## Leveraging Python protocols

The Python SDK for Infrahub has been designed to automatically work with any schemas loaded into Infrahub.
Internally, the Python SDK generates dynamic Python representations of your schemas.

While this approach improves code readability, it presents challenges with type checking because each object has a different signature based on your schema.

### Without protocols

In the example below, type checkers like Mypy will typically complain about `blue_tag.description.value` because `description` is a dynamic parameter generated by the SDK.

```python
# Type checker cannot verify the existence of 'description'
blue_tag = client.get("BuiltinTag", name__value="blue")  # blue_tag is of type InfrahubNode or InfrahubNodeSync
blue_tag.description.value = "The blue tag"  # Mypy: error: "InfrahubNode" has no attribute "description"
blue_tag.save()
```

### With protocols

To provide strict type checking while maintaining platform extensibility, the Python SDK integrates with Python Protocols.

For all core and internal models, the protocols are included in the SDK under `infrahub_sdk.protocols`.
Whenever you need to specify the kind of object you're working with as a string, you can use the corresponding protocol instead.

```python
from infrahub_sdk.protocols import BuiltinTag

# Type checker can now verify all attributes
blue_tag = client.get(BuiltinTag, name__value="blue")  # blue_tag is of type BuiltinTag
blue_tag.description.value = "The blue tag"  # No type errors
blue_tag.save()
```

:::note Python Protocols

Python Protocols, introduced in PEP 544, define a set of method and property signatures that a class must implement to be considered a match, enabling structural subtyping (also known as "duck typing" with static checks). They allow you to specify behavior without requiring inheritance, making code more flexible and type-safe.

More information about Python Protocols can be found [here](https://typing.python.org/en/latest/spec/protocol.html)

:::

## Generating custom protocols based on your schema

You can generate Python Protocols for your own models using the `infrahubctl protocols` command. This supports both synchronous and asynchronous Python code.

It's possible to provide the schema from a local directory or from an existing Infrahub Instance.

<Tabs groupId="local-remote">
  <TabItem value="Existing Infrahub Instance" default>

  ```shell
  export INFRAHUB_ADDRESS=https://infrahub.example.com
  infrahubctl protocols --out lib/protocols.py --sync
  ```

  </TabItem>
  <TabItem value="Local Directory" default>

  ```shell
  infrahubctl protocols --schemas schemas/tag.schema.yml --out lib/protocols.py
  ```

  </TabItem>
</Tabs>

> When using a local directory, Protocols for Profiles and Object Templates won't be generated.

## Using custom protocols

After generation, you can import and use your custom protocols as describe below.

```python
from lib.protocols import MyOwnObject

# Use your custom protocol
my_object = client.get(MyOwnObject, name__value="example")
```

> if you don't have your own Python module, it's possible to use relative path by having the `protocols.py` in the same directory as your script/transform/generator

## Generating Pydantic models from GraphQL queries

When working with GraphQL queries, you can generate type-safe Pydantic models that correspond to your query return types. This provides excellent type safety and IDE support for your GraphQL operations.

### Why use generated return types?

Generated Pydantic models from GraphQL queries offer several important benefits:

- **Type Safety**: Catch type errors during development time instead of at runtime
- **IDE Support**: Get autocomplete, type hints, and better IntelliSense in your IDE
- **Documentation**: Generated models serve as living documentation of your GraphQL API
- **Validation**: Automatic validation of query responses against the expected schema

### Generating return types

Use the `infrahubctl graphql generate-return-types` command to create Pydantic models from your GraphQL queries:

```shell
# Generate models for queries in a directory
infrahubctl graphql generate-return-types queries/

# Generate models for specific query files
infrahubctl graphql generate-return-types queries/get_tags.gql
```

> You can also export the GraphQL schema first using the `infrahubctl graphql export-schema` command.

### Example workflow

1. **Create your GraphQL queries** in `.gql` files preferably in a directory (for example, `queries/`):

   ```graphql
   # queries/get_tags.gql
   query GetAllTags {
      BuiltinTag {
         edges {
            node {
               __typename
               name {
                  value
               }
            }
         }
      }
   }
   ```

2. **Export the GraphQL schema**:

   ```shell
   infrahubctl graphql export-schema
   ```

3. **Generate the Pydantic models**:

   ```shell
   infrahubctl graphql generate-return-types queries/
   ```

   :::warning Query names

   Ensure each of your GraphQL queries has a unique name, as the generated Python files will be named based on these query names.
   Two queries with the same name will land in the same file, leading to potential overrides.

   :::

4. **Use the generated models** in your Python code

   ```python
   from .queries.get_tags import GetAllTagsQuery

   response = await client.execute_graphql(query=MY_QUERY)
   data = GetAllTagsQuery(**response)
   ```
