Repository: opsmill/infrahub-sdk-python

Issue:
bug: TypeError when executing CTL
### Component

infrahubctl

### Infrahub SDK version

1.4.0-1.7.0

### Current Behavior

Whenever attempting to use InfrahubCTL commands with Python 3.9, we encounter Typing issues on execution such as executing `infrahubctl version` encounters the following error:

```python
❯ infrahubctl version
╭──────────────────────────────────────────────────────────────────────────────────────────────────────── Traceback (most recent call last) ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ /Users/Mikhail/cloned/infrahub-sdk-python/.venv/bin/infrahubctl:6 in <module>                                                                                                                                                                      │
│                                                                                                                                                                                                                                                    │
│   3 from infrahub_sdk.ctl.cli import app                                                                                                                                                                                                           │
│   4                                                                                                                                                                                                                                                │
│   5 if __name__ == '__main__':                                                                                                                                                                                                                     │
│ ❱ 6 │   sys.exit(app())                                                                                                                                                                                                                            │
│   7                                                                                                                                                                                                                                                │
│                                                                                                                                                                                                                                                    │
│ /Users/Mikhail/cloned/infrahub-sdk-python/.venv/lib/python3.9/site-packages/typer/main.py:340 in __call__                                                                                                                                          │
│                                                                                                                                                                                                                                                    │
│ /Users/Mikhail/cloned/infrahub-sdk-python/.venv/lib/python3.9/site-packages/typer/main.py:323 in __call__                                                                                                                                          │
│                                                                                                                                                                                                                                                    │
│ /Users/Mikhail/cloned/infrahub-sdk-python/.venv/lib/python3.9/site-packages/typer/main.py:362 in get_command                                                                                                                                       │
│                                                                                                                                                                                                                                                    │
│ /Users/Mikhail/cloned/infrahub-sdk-python/.venv/lib/python3.9/site-packages/typer/main.py:344 in get_group                                                                                                                                         │
│                                                                                                                                                                                                                                                    │
│ /Users/Mikhail/cloned/infrahub-sdk-python/.venv/lib/python3.9/site-packages/typer/main.py:481 in get_group_from_info                                                                                                                               │
│                                                                                                                                                                                                                                                    │
│ /Users/Mikhail/cloned/infrahub-sdk-python/.venv/lib/python3.9/site-packages/typer/main.py:585 in get_command_from_info                                                                                                                             │
│                                                                                                                                                                                                                                                    │
│ /Users/Mikhail/cloned/infrahub-sdk-python/.venv/lib/python3.9/site-packages/typer/main.py:556 in get_params_convertors_ctx_param_name_from_function                                                                                                │
│                                                                                                                                                                                                                                                    │
│ /Users/Mikhail/cloned/infrahub-sdk-python/.venv/lib/python3.9/site-packages/typer/utils.py:114 in get_params_from_function                                                                                                                         │
│                                                                                                                                                                                                                                                    │
│ /Users/Mikhail/cloned/infrahub-sdk-python/.venv/lib/python3.9/site-packages/typing_extensions.py:1230 in get_type_hints                                                                                                                            │
│                                                                                                                                                                                                                                                    │
│   1227 │   │     locals, respectively.                                                                                                                                                                                                             │
│   1228 │   │   """                                                                                                                                                                                                                                 │
│   1229 │   │   if hasattr(typing, "Annotated"):  # 3.9+                                                                                                                                                                                            │
│ ❱ 1230 │   │   │   hint = typing.get_type_hints(                                                                                                                                                                                                   │
│   1231 │   │   │   │   obj, globalns=globalns, localns=localns, include_extras=True                                                                                                                                                                │
│   1232 │   │   │   )                                                                                                                                                                                                                               │
│   1233 │   │   else:  # 3.8                                                                                                                                                                                                                        │
│                                                                                                                                                                                                                                                    │
│ /Users/Mikhail/.pyenv/versions/3.9.21/lib/python3.9/typing.py:1497 in get_type_hints                                                                                                                                                               │
│                                                                                                                                                                                                                                                    │
│   1494 │   │   │   │   is_argument=not isinstance(obj, types.ModuleType),                                                                                                                                                                          │
│   1495 │   │   │   │   is_class=False,                                                                                                                                                                                                             │
│   1496 │   │   │   )                                                                                                                                                                                                                               │
│ ❱ 1497 │   │   value = _eval_type(value, globalns, localns)                                                                                                                                                                                        │
│   1498 │   │   if name in defaults and defaults[name] is None:                                                                                                                                                                                     │
│   1499 │   │   │   value = Optional[value]                                                                                                                                                                                                         │
│   1500 │   │   hints[name] = value                                                                                                                                                                                                                 │
│                                                                                                                                                                                                                                                    │
│ /Users/Mikhail/.pyenv/versions/3.9.21/lib/python3.9/typing.py:292 in _eval_type                                                                                                                                                                    │
│                                                                                                                                                                                                                                                    │
│    289 │   ForwardRef.                                                                                                                                                                                                                             │
│    290 │   """                                                                                                                                                                                                                                     │
│    291 │   if isinstance(t, ForwardRef):                                                                                                                                                                                                           │
│ ❱  292 │   │   return t._evaluate(globalns, localns, recursive_guard)                                                                                                                                                                              │
│    293 │   if isinstance(t, (_GenericAlias, GenericAlias)):                                                                                                                                                                                        │
│    294 │   │   ev_args = tuple(_eval_type(a, globalns, localns, recursive_guard) for a in t.__a                                                                                                                                                    │
│    295 │   │   if ev_args == t.__args__:                                                                                                                                                                                                           │
│                                                                                                                                                                                                                                                    │
│ /Users/Mikhail/.pyenv/versions/3.9.21/lib/python3.9/typing.py:554 in _evaluate                                                                                                                                                                     │
│                                                                                                                                                                                                                                                    │
│    551 │   │   │   │   │   sys.modules.get(self.__forward_module__, None), '__dict__', globalns                                                                                                                                                    │
│    552 │   │   │   │   )                                                                                                                                                                                                                           │
│    553 │   │   │   type_ = _type_check(                                                                                                                                                                                                            │
│ ❱  554 │   │   │   │   eval(self.__forward_code__, globalns, localns),                                                                                                                                                                             │
│    555 │   │   │   │   "Forward references must evaluate to types.",                                                                                                                                                                               │
│    556 │   │   │   │   is_argument=self.__forward_is_argument__,                                                                                                                                                                                   │
│    557 │   │   │   │   allow_special_forms=self.__forward_is_class__,                                                                                                                                                                              │
│ in <module>:1                                                                                                                                                                                                                                      │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'
```

### Expected Behavior

To be able to use Infrahubctl with Python 3.9 (or set minimum Python version to 3.10)

### Steps to Reproduce

1. Create venv with Python 3.9
2. Install latest `pip install "infrahub-sdk[ctl]"` or `poetry install` on this repo.
3. Execute `infrahubctl version

### Additional Information

Some related issues and if you uninstall `eval-type-backport` package and run the `infrahubctl`, it'll tell you to install it.
https://github.com/pydantic/pydantic/issues/7873
https://github.com/alexmojaki/eval_type_backport

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill) Report
Attention: Patch coverage is `0%` with `8 lines` in your changes missing coverage. Please review.

| [Files with missing lines](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill) | Patch % | Lines |
|---|---|---|
| [infrahub\_sdk/ctl/utils.py](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&filepath=infrahub_sdk%2Fctl%2Futils.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill#diff-aW5mcmFodWJfc2RrL2N0bC91dGlscy5weQ==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill) |
| [infrahub\_sdk/ctl/check.py](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&filepath=infrahub_sdk%2Fctl%2Fcheck.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill#diff-aW5mcmFodWJfc2RrL2N0bC9jaGVjay5weQ==) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill) |
| [infrahub\_sdk/ctl/cli\_commands.py](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&filepath=infrahub_sdk%2Fctl%2Fcli_commands.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill#diff-aW5mcmFodWJfc2RrL2N0bC9jbGlfY29tbWFuZHMucHk=) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill) |
| [infrahub\_sdk/ctl/generator.py](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&filepath=infrahub_sdk%2Fctl%2Fgenerator.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill#diff-aW5mcmFodWJfc2RrL2N0bC9nZW5lcmF0b3IucHk=) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill) |
| [infrahub\_sdk/ctl/importer.py](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&filepath=infrahub_sdk%2Fctl%2Fimporter.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill#diff-aW5mcmFodWJfc2RrL2N0bC9pbXBvcnRlci5weQ==) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill) |
| [infrahub\_sdk/ctl/repository.py](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&filepath=infrahub_sdk%2Fctl%2Frepository.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill#diff-aW5mcmFodWJfc2RrL2N0bC9yZXBvc2l0b3J5LnB5) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill) |
| [infrahub\_sdk/ctl/validate.py](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&filepath=infrahub_sdk%2Fctl%2Fvalidate.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill#diff-aW5mcmFodWJfc2RrL2N0bC92YWxpZGF0ZS5weQ==) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill) |

> :exclamation:  There is a different number of reports uploaded between BASE (7f1ebbb) and HEAD (3782d1c). Click for more details.
> 
> <details><summary>HEAD has 6 uploads less than BASE</summary>
>
>| Flag | BASE (7f1ebbb) | HEAD (3782d1c) |
>|------|------|------|
>|python-3.13|1|0|
>|python-filler-3.12|1|0|
>|python-3.10|1|0|
>|python-3.11|1|0|
>|python-3.12|1|0|
>|python-3.9|1|0|
></details>

```diff
@@             Coverage Diff             @@
##           stable     #259       +/-   ##
===========================================
- Coverage   70.36%   22.45%   -47.91%     
===========================================
  Files          82       82               
  Lines        7497     7500        +3     
  Branches     1389     1387        -2     
===========================================
- Hits         5275     1684     -3591     
- Misses       1848     5654     +3806     
+ Partials      374      162      -212     
```

| [Flag](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill) | Coverage Δ | |
|---|---|---|
| [integration-tests](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill) | `22.45% <0.00%> (-0.01%)` | :arrow_down: |
| [python-3.10](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill) | `?` | |
| [python-3.11](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill) | `?` | |
| [python-3.12](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill) | `?` | |
| [python-3.13](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill) | `?` | |
| [python-3.9](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill) | `?` | |
| [python-filler-3.12](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill) | `?` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files with missing lines](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill) | Coverage Δ | |
|---|---|---|
| [infrahub\_sdk/ctl/check.py](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&filepath=infrahub_sdk%2Fctl%2Fcheck.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill#diff-aW5mcmFodWJfc2RrL2N0bC9jaGVjay5weQ==) | `0.00% <0.00%> (-27.05%)` | :arrow_down: |
| [infrahub\_sdk/ctl/cli\_commands.py](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&filepath=infrahub_sdk%2Fctl%2Fcli_commands.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill#diff-aW5mcmFodWJfc2RrL2N0bC9jbGlfY29tbWFuZHMucHk=) | `0.00% <0.00%> (-58.83%)` | :arrow_down: |
| [infrahub\_sdk/ctl/generator.py](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&filepath=infrahub_sdk%2Fctl%2Fgenerator.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill#diff-aW5mcmFodWJfc2RrL2N0bC9nZW5lcmF0b3IucHk=) | `0.00% <0.00%> (-23.64%)` | :arrow_down: |
| [infrahub\_sdk/ctl/importer.py](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&filepath=infrahub_sdk%2Fctl%2Fimporter.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill#diff-aW5mcmFodWJfc2RrL2N0bC9pbXBvcnRlci5weQ==) | `0.00% <0.00%> (-57.15%)` | :arrow_down: |
| [infrahub\_sdk/ctl/repository.py](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&filepath=infrahub_sdk%2Fctl%2Frepository.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill#diff-aW5mcmFodWJfc2RrL2N0bC9yZXBvc2l0b3J5LnB5) | `0.00% <0.00%> (-80.56%)` | :arrow_down: |
| [infrahub\_sdk/ctl/validate.py](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&filepath=infrahub_sdk%2Fctl%2Fvalidate.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill#diff-aW5mcmFodWJfc2RrL2N0bC92YWxpZGF0ZS5weQ==) | `0.00% <0.00%> (-52.86%)` | :arrow_down: |
| [infrahub\_sdk/ctl/utils.py](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259?src=pr&el=tree&filepath=infrahub_sdk%2Fctl%2Futils.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill#diff-aW5mcmFodWJfc2RrL2N0bC91dGlscy5weQ==) | `0.00% <0.00%> (-66.43%)` | :arrow_down: |

... and [69 files with indirect coverage changes](https://app.codecov.io/gh/opsmill/infrahub-sdk-python/pull/259/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opsmill)


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d5651ad..d0dc95f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -126,7 +126,6 @@ jobs:
           - "3.10"
           - "3.11"
           - "3.12"
-          - "3.13"
     if: |
       always() && !cancelled() &&
       !contains(needs.*.result, 'failure') &&
@@ -144,7 +143,7 @@ jobs:
           python-version: ${{ matrix.python-version }}
       - name: "Setup environment"
         run: |
-          pipx install poetry==1.8.5
+          pipx install poetry==1.8.5 --python python${{ matrix.python-version }}
           poetry config virtualenvs.create true --local
           pip install invoke toml codecov
       - name: "Install Package"
diff --git a/changelog/251.fixed.md b/changelog/251.fixed.md
new file mode 100644
index 0000000..84e3916
--- /dev/null
+++ b/changelog/251.fixed.md
@@ -0,0 +1,1 @@
+Fix typing for Python 3.9 and remove support for Python 3.13
\ No newline at end of file
diff --git a/infrahub_sdk/ctl/check.py b/infrahub_sdk/ctl/check.py
index 3d5319d..0626d88 100644
--- a/infrahub_sdk/ctl/check.py
+++ b/infrahub_sdk/ctl/check.py
@@ -5,7 +5,7 @@ import sys
 from asyncio import run as aiorun
 from dataclasses import dataclass
 from pathlib import Path
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Optional
 
 import typer
 from rich.console import Console
@@ -50,8 +50,8 @@ def run(
     format_json: bool,
     list_available: bool,
     variables: dict[str, str],
-    name: str | None = None,
-    branch: str | None = None,
+    name: Optional[str] = None,
+    branch: Optional[str] = None,
 ) -> None:
     """Locate and execute all checks under the defined path."""
 
diff --git a/infrahub_sdk/ctl/cli_commands.py b/infrahub_sdk/ctl/cli_commands.py
index 19980eb..7a10f13 100644
--- a/infrahub_sdk/ctl/cli_commands.py
+++ b/infrahub_sdk/ctl/cli_commands.py
@@ -7,7 +7,7 @@ import logging
 import platform
 import sys
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Callable
+from typing import TYPE_CHECKING, Any, Callable, Optional
 
 import jinja2
 import typer
@@ -74,13 +74,13 @@ console = Console()
 @catch_exception(console=console)
 def check(
     check_name: str = typer.Argument(default="", help="Name of the Python check"),
-    branch: str | None = None,
+    branch: Optional[str] = None,
     path: str = typer.Option(".", help="Root directory"),
     debug: bool = False,
     format_json: bool = False,
     _: str = CONFIG_PARAM,
     list_available: bool = typer.Option(False, "--list", help="Show available Python checks"),
-    variables: list[str] | None = typer.Argument(
+    variables: Optional[list[str]] = typer.Argument(
         None, help="Variables to pass along with the query. Format key=value key=value."
     ),
 ) -> None:
@@ -102,12 +102,12 @@ def check(
 @catch_exception(console=console)
 async def generator(
     generator_name: str = typer.Argument(default="", help="Name of the Generator"),
-    branch: str | None = None,
+    branch: Optional[str] = None,
     path: str = typer.Option(".", help="Root directory"),
     debug: bool = False,
     _: str = CONFIG_PARAM,
     list_available: bool = typer.Option(False, "--list", help="Show available Generators"),
-    variables: list[str] | None = typer.Argument(
+    variables: Optional[list[str]] = typer.Argument(
         None, help="Variables to pass along with the query. Format key=value key=value."
     ),
 ) -> None:
@@ -130,13 +130,13 @@ async def run(
     debug: bool = False,
     _: str = CONFIG_PARAM,
     branch: str = typer.Option(None, help="Branch on which to run the script."),
-    concurrent: int | None = typer.Option(
+    concurrent: Optional[int] = typer.Option(
         None,
         help="Maximum number of requests to execute at the same time.",
         envvar="INFRAHUB_MAX_CONCURRENT_EXECUTION",
     ),
     timeout: int = typer.Option(60, help="Timeout in sec", envvar="INFRAHUB_TIMEOUT"),
-    variables: list[str] | None = typer.Argument(
+    variables: Optional[list[str]] = typer.Argument(
         None, help="Variables to pass along with the query. Format key=value key=value."
     ),
 ) -> None:
@@ -259,7 +259,7 @@ def _run_transform(
 @catch_exception(console=console)
 def render(
     transform_name: str = typer.Argument(default="", help="Name of the Python transformation", show_default=False),
-    variables: list[str] | None = typer.Argument(
+    variables: Optional[list[str]] = typer.Argument(
         None, help="Variables to pass along with the query. Format key=value key=value."
     ),
     branch: str = typer.Option(None, help="Branch on which to render the transform."),
@@ -309,7 +309,7 @@ def render(
 @catch_exception(console=console)
 def transform(
     transform_name: str = typer.Argument(default="", help="Name of the Python transformation", show_default=False),
-    variables: list[str] | None = typer.Argument(
+    variables: Optional[list[str]] = typer.Argument(
         None, help="Variables to pass along with the query. Format key=value key=value."
     ),
     branch: str = typer.Option(None, help="Branch on which to run the transformation"),
diff --git a/infrahub_sdk/ctl/generator.py b/infrahub_sdk/ctl/generator.py
index 5258caa..2250156 100644
--- a/infrahub_sdk/ctl/generator.py
+++ b/infrahub_sdk/ctl/generator.py
@@ -1,7 +1,7 @@
 from __future__ import annotations
 
 from pathlib import Path
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Optional
 
 import typer
 from rich.console import Console
@@ -23,7 +23,7 @@ async def run(
     debug: bool,
     list_available: bool,
     branch: str | None = None,
-    variables: list[str] | None = None,
+    variables: Optional[list[str]] = None,
 ) -> None:
     init_logging(debug=debug)
     repository_config = get_repository_config(Path(config.INFRAHUB_REPO_CONFIG_FILE))
diff --git a/infrahub_sdk/ctl/importer.py b/infrahub_sdk/ctl/importer.py
index 4645fa0..e9181c8 100644
--- a/infrahub_sdk/ctl/importer.py
+++ b/infrahub_sdk/ctl/importer.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 from asyncio import run as aiorun
 from pathlib import Path
+from typing import Optional
 
 import typer
 from rich.console import Console
@@ -26,7 +27,7 @@ def load(
     quiet: bool = typer.Option(False, help="No console output"),
     _: str = CONFIG_PARAM,
     branch: str = typer.Option(None, help="Branch from which to export"),
-    concurrent: int | None = typer.Option(
+    concurrent: Optional[int] = typer.Option(
         None,
         help="Maximum number of requests to execute at the same time.",
         envvar="INFRAHUB_MAX_CONCURRENT_EXECUTION",
diff --git a/infrahub_sdk/ctl/repository.py b/infrahub_sdk/ctl/repository.py
index 4e6d353..1992a53 100644
--- a/infrahub_sdk/ctl/repository.py
+++ b/infrahub_sdk/ctl/repository.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 from pathlib import Path
+from typing import Optional
 
 import typer
 import yaml
@@ -68,7 +69,7 @@ async def add(
     name: str,
     location: str,
     description: str = "",
-    username: str | None = None,
+    username: Optional[str] = None,
     password: str = "",
     ref: str = "",
     read_only: bool = False,
@@ -114,7 +115,7 @@ async def add(
 
 @app.command()
 async def list(
-    branch: str = typer.Option(None, help="Branch on which to list repositories."),
+    branch: Optional[str] = typer.Option(None, help="Branch on which to list repositories."),
     debug: bool = False,
     _: str = CONFIG_PARAM,
 ) -> None:
diff --git a/infrahub_sdk/ctl/utils.py b/infrahub_sdk/ctl/utils.py
index e4474d8..4c62711 100644
--- a/infrahub_sdk/ctl/utils.py
+++ b/infrahub_sdk/ctl/utils.py
@@ -6,7 +6,7 @@ import traceback
 from collections.abc import Coroutine
 from functools import wraps
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Callable, NoReturn, TypeVar
+from typing import TYPE_CHECKING, Any, Callable, NoReturn, Optional, TypeVar
 
 import pendulum
 import typer
@@ -145,7 +145,7 @@ def print_graphql_errors(console: Console, errors: list) -> None:
             console.print(f"[red]{escape(str(error))}")
 
 
-def parse_cli_vars(variables: list[str] | None) -> dict[str, str]:
+def parse_cli_vars(variables: Optional[list[str]]) -> dict[str, str]:
     if not variables:
         return {}
 
diff --git a/infrahub_sdk/ctl/validate.py b/infrahub_sdk/ctl/validate.py
index d93260b..9931823 100644
--- a/infrahub_sdk/ctl/validate.py
+++ b/infrahub_sdk/ctl/validate.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 import sys
 from pathlib import Path
+from typing import Optional
 
 import typer
 import ujson
@@ -57,7 +58,7 @@ async def validate_schema(schema: Path, _: str = CONFIG_PARAM) -> None:
 @catch_exception(console=console)
 def validate_graphql(
     query: str,
-    variables: list[str] | None = typer.Argument(
+    variables: Optional[list[str]] = typer.Argument(
         None, help="Variables to pass along with the query. Format key=value key=value."
     ),
     debug: bool = typer.Option(False, help="Display more troubleshooting information."),
diff --git a/poetry.lock b/poetry.lock
index fc23269..3b60de6 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1301,10 +1301,7 @@ files = [
 [package.dependencies]
 annotated-types = ">=0.6.0"
 pydantic-core = "2.23.3"
-typing-extensions = [
-    {version = ">=4.6.1", markers = "python_version < \"3.13\""},
-    {version = ">=4.12.2", markers = "python_version >= \"3.13\""},
-]
+typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""}
 
 [package.extras]
 email = ["email-validator (>=2.0.0)"]
@@ -2237,5 +2234,5 @@ tests = ["Jinja2", "pytest", "pyyaml", "rich"]
 
 [metadata]
 lock-version = "2.0"
-python-versions = "^3.9"
-content-hash = "8df86b45a0478a859bbe784b5d9027b2f033da597a52815f6006fd81fd424fdf"
+python-versions = "^3.9, < 3.13"
+content-hash = "7cf3b9fd5e6ad627c30cb1660ef9c45d5b6a264150d064bc47cc7ae7a2be4030"
diff --git a/pyproject.toml b/pyproject.toml
index 7cc22f4..9fbab36 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,11 +16,10 @@ classifiers = [
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.12",
-    "Programming Language :: Python :: 3.13",
 ]
 
 [tool.poetry.dependencies]
-python = "^3.9"
+python = "^3.9, < 3.13"
 pydantic = ">=2.0.0,!=2.0.1,!=2.1.0,<3.0.0"
 pydantic-settings = ">=2.0"
 graphql-core = ">=3.1,<3.3"
@@ -248,6 +247,10 @@ max-complexity = 17
     "ANN401", # Dynamically typed expressions (typing.Any) are disallowed
 ]
 
+"infrahub_sdk/ctl/**/*.py" = [
+    "UP007",  # Use `X | Y` for type annotations | Required for Typer until we can drop the support for Python 3.9
+]
+
 "infrahub_sdk/client.py" = [
     ##################################################################################################
     # Review and change the below later                                                              #


Test patch:
diff --git a/tests/unit/ctl/test_cli.py b/tests/unit/ctl/test_cli.py
index d73a710..30d63e9 100644
--- a/tests/unit/ctl/test_cli.py
+++ b/tests/unit/ctl/test_cli.py
@@ -1,16 +1,10 @@
-import sys
-
-import pytest
 from typer.testing import CliRunner
 
 from infrahub_sdk.ctl.cli import app
 
 runner = CliRunner()
 
-requires_python_310 = pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires Python 3.10 or higher")
-
 
-@requires_python_310
 def test_main_app():
     result = runner.invoke(app, ["--help"])
     assert result.exit_code == 0
@@ -29,14 +23,12 @@ def test_validate_all_groups_have_names():
         assert group.name
 
 
-@requires_python_310
 def test_version_command():
     result = runner.invoke(app, ["version"])
     assert result.exit_code == 0
     assert "Python SDK: v" in result.stdout
 
 
-@requires_python_310
 def test_info_command_success(mock_query_infrahub_version, mock_query_infrahub_user):
     result = runner.invoke(app, ["info"])
     assert result.exit_code == 0
@@ -44,14 +36,12 @@ def test_info_command_success(mock_query_infrahub_version, mock_query_infrahub_u
         assert expected in result.stdout, f"'{expected}' not found in info command output"
 
 
-@requires_python_310
 def test_info_command_failure():
     result = runner.invoke(app, ["info"])
     assert result.exit_code == 0
     assert "Connection Error" in result.stdout
 
 
-@requires_python_310
 def test_info_detail_command_success(mock_query_infrahub_version, mock_query_infrahub_user):
     result = runner.invoke(app, ["info", "--detail"])
     assert result.exit_code == 0
@@ -65,7 +55,6 @@ def test_info_detail_command_success(mock_query_infrahub_version, mock_query_inf
         assert expected in result.stdout, f"'{expected}' not found in detailed info command output"
 
 
-@requires_python_310
 def test_info_detail_command_failure():
     result = runner.invoke(app, ["info", "--detail"])
     assert result.exit_code == 0
diff --git a/tests/unit/ctl/test_repository_app.py b/tests/unit/ctl/test_repository_app.py
index 7fb01ee..ba0a772 100644
--- a/tests/unit/ctl/test_repository_app.py
+++ b/tests/unit/ctl/test_repository_app.py
@@ -1,6 +1,5 @@
 """Integration tests for infrahubctl commands."""
 
-import sys
 from unittest import mock
 
 import pytest
@@ -13,8 +12,6 @@ from tests.helpers.utils import strip_color
 
 runner = CliRunner()
 
-requires_python_310 = pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires Python 3.10 or higher")
-
 
 @pytest.fixture
 def mock_client() -> mock.Mock:
@@ -29,7 +26,6 @@ def mock_client() -> mock.Mock:
 class TestInfrahubctlRepository:
     """Groups the 'infrahubctl repository' test cases."""
 
-    @requires_python_310
     @mock.patch("infrahub_sdk.ctl.repository.initialize_client")
     def test_repo_no_username_or_password(self, mock_init_client, mock_client) -> None:
         """Case allow no username to be passed in and set it as None rather than blank string that fails."""
@@ -77,7 +73,6 @@ mutation {
             tracker="mutation-repository-create",
         )
 
-    @requires_python_310
     @mock.patch("infrahub_sdk.ctl.repository.initialize_client")
     def test_repo_no_username(self, mock_init_client, mock_client) -> None:
         """Case allow no username to be passed in and set it as None rather than blank string that fails."""
@@ -137,7 +132,6 @@ mutation {
             tracker="mutation-repository-create",
         )
 
-    @requires_python_310
     @mock.patch("infrahub_sdk.ctl.repository.initialize_client")
     def test_repo_username(self, mock_init_client, mock_client) -> None:
         """Case allow no username to be passed in and set it as None rather than blank string that fails."""
@@ -199,7 +193,6 @@ mutation {
             tracker="mutation-repository-create",
         )
 
-    @requires_python_310
     @mock.patch("infrahub_sdk.ctl.repository.initialize_client")
     def test_repo_readonly_true(self, mock_init_client, mock_client) -> None:
         """Case allow no username to be passed in and set it as None rather than blank string that fails."""
@@ -260,7 +253,6 @@ mutation {
             tracker="mutation-repository-create",
         )
 
-    @requires_python_310
     @mock.patch("infrahub_sdk.ctl.repository.initialize_client")
     def test_repo_description_commit_branch(self, mock_init_client, mock_client) -> None:
         """Case allow no username to be passed in and set it as None rather than blank string that fails."""
diff --git a/tests/unit/ctl/test_validate_app.py b/tests/unit/ctl/test_validate_app.py
index 3264b08..43d41dd 100644
--- a/tests/unit/ctl/test_validate_app.py
+++ b/tests/unit/ctl/test_validate_app.py
@@ -1,5 +1,3 @@
-import sys
-
 import pytest
 from typer.testing import CliRunner
 
@@ -9,10 +7,7 @@ from tests.helpers.cli import remove_ansi_color
 
 runner = CliRunner()
 
-requires_python_310 = pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires Python 3.10 or higher")
-
 
-@requires_python_310
 def test_validate_schema_valid():
     fixture_file = get_fixtures_dir() / "models" / "valid_model_01.json"
 
@@ -21,7 +16,6 @@ def test_validate_schema_valid():
     assert "Schema is valid" in result.stdout
 
 
-@requires_python_310
 def test_validate_schema_empty():
     fixture_file = get_fixtures_dir() / "models" / "empty.json"
 
@@ -30,7 +24,6 @@ def test_validate_schema_empty():
     assert "Empty YAML/JSON file" in remove_ansi_color(result.stdout)
 
 
-@requires_python_310
 def test_validate_schema_non_valid():
     fixture_file = get_fixtures_dir() / "models" / "non_valid_model_01.json"