# Python SDK migration guide: v0.4 to v0.5

This guide will help you migrate your Inngest Python SDK from v0.4 to v0.5.

## New features

- First-class Pydantic support in step and function output ([docs](/docs-markdown/reference/python/guides/pydantic))
- Python 3.13 support
- Function singletons ([docs](/docs-markdown/guides/singleton))
- Function timeouts ([docs](/docs-markdown/features/inngest-functions/cancellation/cancel-on-timeouts))
- Experimental step.infer ([docs](/docs-markdown/features/inngest-functions/steps-workflows/step-ai-orchestration))
- Improved parallel step performance

## Breaking changes

### Move `step` into `ctx`

The `step` object will be moved to `ctx.step`.

Before:

```py
@inngest_client.create_function(
    fn_id="provision-user",
    trigger=inngest.TriggerEvent(event="user.signup"),
)
async def fn(ctx: inngest.Context, step: inngest.Step) -> None:
    await step.run("create-user", create_db_user)
```

After:

```py
@inngest_client.create_function(
    fn_id="provision-user",
    trigger=inngest.TriggerEvent(event="user.signup"),
)
async def fn(ctx: inngest.Context) -> None:
    await ctx.step.run("create-user", create_db_user)
```

### Parallel steps

`step.parallel` will be removed in favor of a new `ctx.group.parallel` method. This method will behave the same way, so it's a drop-in replacement for `step.parallel`.

```py
@inngest_client.create_function(
    fn_id="my-fn",
    trigger=inngest.TriggerEvent(event="my-event"),
)
async def fn( ctx: inngest.Context) -> None:
    user_id = ctx.event.data["user_id"]

    await ctx.group.parallel(
        (
            lambda: ctx.step.run("update-user", update_user, user_id),
            lambda: ctx.step.run("send-email", send_email, user_id),
        )
    )
```

### Remove `event.user`

We're sunsetting `event.user`. It's already incompatible with some features (e.g. function run replay).

### Disallow mixed async-ness within Inngest functions

Setting an async `on_failure` on a non-async Inngest function will throw an error:

```py
async def on_failure(ctx: inngest.Context) -> None:
    pass

@inngest_client.create_function(
    fn_id="foo",
    trigger=inngest.TriggerEvent(event="foo"),
    on_failure=on_failure,
)
def fn(ctx: inngest.ContextSync) -> None:
    pass
```

Setting a non-async `on_failure` on an async Inngest function will throw an error:

```py
def on_failure(ctx: inngest.ContextSync) -> None:
    pass

@inngest_client.create_function(
    fn_id="foo",
    trigger=inngest.TriggerEvent(event="foo"),
    on_failure=on_failure,
)
async def fn(ctx: inngest.Context) -> None:
    pass
```

### Static error when passing a non-async callback to an async `step.run`

When passing a non-async callback to an async `step.run`, it will work at runtime but there will be a static type error.

```py
@inngest_client.create_function(
    fn_id="foo",
    trigger=inngest.TriggerEvent(event="foo"),
)
async def fn(ctx: inngest.Context) -> None:
    # Type error because `lambda: "hello"` is non-async.
    msg = await ctx.step.run("step", lambda: "hello")

    # Runtime value is "hello", as expected.
    print(msg)
```

### `inngest.Function` is generic

The `inngest.Function` class is now a generic that represents the return type. So if an Inngest function returns `str` then it would be `inngest.Function[str]`.

### Middleware order

Use LIFO for the "after" hooks. In other words, when multiple middleware is specified then the "after" hooks are run in reverse order.

For example, let's say the following middleware is defined and used:

```py
class A(inngest.MiddlewareSync):
    def before_execution(self) -> None:
        pass

    def after_execution(self) -> None:
        pass

class B(inngest.MiddlewareSync):
    def before_execution(self) -> None:
        pass

    def after_execution(self) -> None:
        pass

inngest.Inngest(
    app_id="my-app",
    middleware=[A, B],
)
```

The middleware will be executed in the following order for each hook:

- `before_execution` -- `A` then `B`.
- `after_execution` -- `B` then `A`.

The "before" hooks are:

```
before_execution
before_response
before_send_events
transform_input
```

The "after" hooks are:

```
after_execution
after_send_events
transform_output
```

### Remove middleware hooks

- `before_memoization`
- `after_memoization`

### Remove experimental stuff

- `inngest.experimental.encryption_middleware` (it's now the [inngest-encryption](https://pypi.org/project/inngest-encryption/) package).
- `experimental_execution` option on functions. We won't support native `asyncio` methods (e.g. `asyncio.gather`) going forward.

### Dependencies

Drop support for Python `3.9`.

Bump dependency minimum versions:

```
httpx>=0.26.0
pydantic>=2.11.0
typing-extensions>=4.13.0
```

Bump peer dependency minimum versions:

```
Django>=5.0
Flask>=3.0.0
fastapi>=0.110.0
tornado>=6.4
```