Files
Donald Pinckney b5719bc143 PR Tracking Initial Release (#4)
* Add initial skill for testing, which is simply Steve's skill (#1)

* Add initial skill for testing, which is simply Steve's skill

* Rename skill to 'temporal-dev' and update version

Updated skill name and version for Temporal Python.

* Use claude to merge Steve's, Max's, and Mason's skills.  (#2)

* Use claude to merge Steve's, Max's, and Mason's skills. Did a review pass using claude's skill devlopment skills

* Add missing things from Steve

* trigger tweaks

* Add in common gotchas from Johann

* add simple feedback mechanism (#3)

* Change skill name to kebab-case, for compatibility with Amp and Cline (#7)

* Clean up references/core/ai-integration.md

* Clean up references/core/common-gotchas.md

* Clean up references/core/common-gotchas.md

* Clean up references/core/determinism.md

* Clean up references/core/determinism.md

* Update error-reference.md

* Update interactive-workflows.md

* Clean up patterns.md

* Cut shell scripts

* Edit troubleshooting.md

* remove interceptors for now

* remove dynamic workflows

* clarify on heartbeating of async activity completions, and prompt it a bit in relation to signals

* Improve references/python/advanced-features.md

* Use explicit namespace in connect

* remove duplicated content from determinism.md, clean up

* Improve references/python/data-handling.md

* Prefer start_to_close_timeout

* don't explicitely provide defaults for retry policies

* error-handling.md cleanup

* move idempotency patterns to patterns.md

* remove multi-param activities

* small edits

* Unify sandbox stuff into one file

* local activities aren't experimental

* Clean up references/python/sync-vs-async.md

* Cleanup observability.md, remove duplicated search attributes

* Cut otel for now

* cut a lot of duplicate stuff from python gotchas, address comments

* de-duplicate content

* Lots of improvements to testing

* cleanup to top level of skill (like CLI install instructions), and to top-level of python

* Improve patterns.md

* clean up ai-patterns.md

* Update readme with installation instructions

* remove ts directory

* De-couple core from python and TypeScript as much as possible

* Remove TypeScript hints

* add prompting for feedback at startup - wait for ethan on slack channel

* shorten url

* Update slack channel

* Automated pass over on python cleanup & deduplication

* Remove multi-patching from Python, since its obvious, dont waste tokens on it. (#34)

* Add TypeScript (#31)

Adds initial support for TypeScript to the skill

---------

Co-authored-by: James Watkins-Harvey <mjameswh@users.noreply.github.com>
Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com>

* Fix typos and reference links (#36)

* Fix typos and reference links

* 2 more typo fixes

* quick edit to readme (#37)

* Fix saga compensations to run under cancellation protection (#43)

When a workflow is cancelled mid-saga, compensations must run in a
cancellation-protected scope, otherwise they are immediately cancelled
before they can execute.

- Python: wrap compensation loop in asyncio.shield() so it runs even
  when the workflow receives a CancelledError
- TypeScript: wrap compensation loop in CancellationScope.nonCancellable()
  so it runs even when the root scope is cancelled (per official docs:
  "Cleanup logic must be in a nonCancellable scope")
- TypeScript: also fix compensation registration order — register BEFORE
  calling the activity (was already correct in Python)

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* Update readme for public preview (#45)

* a few more readme tweaks (#46)

* Add MIT License to the project (#47)

* Add Go (supersedes other PR) (#38)

* progress on go

* Go translation workflow completed.

* missed a few spots

* Manual edits

* Address feedback

* Add gotcha about anonymous local activities

* Sample code for payload converter

* clarify sdk protection mechanisms

* Setup CODEOWNERS to AI SDK team (#48)

* Align version number in SKILL.md and plugin.json. (#49)

---------

Co-authored-by: James Watkins-Harvey <mjameswh@users.noreply.github.com>
Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com>
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 17:36:15 -04:00

281 lines
7.3 KiB
Markdown

# Python Gotchas
Python-specific mistakes and anti-patterns. See also [Common Gotchas](references/core/gotchas.md) for language-agnostic concepts.
## File Organization
### Importing Activities into Workflow Files
**The Problem**: The Python sandbox reloads workflow files on every task. Importing heavy activity modules slows down workers.
```python
# BAD - activities.py gets reloaded constantly
# workflows.py
from activities import my_activity
@workflow.defn
class MyWorkflow:
pass
# GOOD - Pass-through import
# workflows.py
from temporalio import workflow
with workflow.unsafe.imports_passed_through():
from activities import my_activity
@workflow.defn
class MyWorkflow:
pass
```
`references/python/determinism-protection.md` contains more info about the Python sandbox.
### Mixing Workflows and Activities
```python
# BAD - Everything in one file
# app.py
@workflow.defn
class MyWorkflow:
@workflow.run
async def run(self):
await workflow.execute_activity(my_activity, ...)
@activity.defn
async def my_activity():
# Heavy imports, I/O, etc.
pass
# GOOD - Separate files
# workflows.py
@workflow.defn
class MyWorkflow:
@workflow.run
async def run(self):
await workflow.execute_activity(my_activity, ...)
# activities.py
@activity.defn
async def my_activity():
pass
```
## Async vs Sync Activities
The Temporal Python SDK supports both async and sync activities. See `references/python/sync-vs-async.md` to understand which to choose. Below are important anti-patterns for both aysnc and sync activities.
### Blocking in Async Activities
```python
# BAD - Blocks the event loop
@activity.defn
async def process_file(path: str) -> str:
with open(path) as f: # Blocking I/O in async!
return f.read()
# GOOD Option 1 - Use sync activity with executor
@activity.defn
def process_file(path: str) -> str:
with open(path) as f:
return f.read()
# Register with executor in worker
Worker(
client,
task_queue="my-queue",
activities=[process_file],
activity_executor=ThreadPoolExecutor(max_workers=10),
)
# GOOD Option 2 - Use async I/O
@activity.defn
async def process_file(path: str) -> str:
async with aiofiles.open(path) as f:
return await f.read()
```
### Missing Executor for Sync Activities
```python
# BAD - Sync activity REQUIRES executor
@activity.defn
def slow_computation(data: str) -> str:
return heavy_cpu_work(data)
Worker(
client,
task_queue="my-queue",
activities=[slow_computation],
# Missing activity_executor! --> THIS IMMEDIATELY RAISES AN EXCEPTION!
)
# GOOD - Provide executor
Worker(
client,
task_queue="my-queue",
activities=[slow_computation],
activity_executor=ThreadPoolExecutor(max_workers=10),
)
```
## Wrong Retry Classification
**Example:** Transient networks errors should be retried. Authentication errors should not be.
See `references/python/error-handling.md` to understand how to classify errors.
## Heartbeating
### Forgetting to Heartbeat Long Activities
```python
# BAD - No heartbeat, can't detect stuck activities
@activity.defn
async def process_large_file(path: str):
async for chunk in read_chunks(path):
process(chunk) # Takes hours, no heartbeat
# GOOD - Regular heartbeats with progress
@activity.defn
async def process_large_file(path: str):
async for i, chunk in enumerate(read_chunks(path)):
activity.heartbeat(f"Processing chunk {i}")
process(chunk)
```
### Heartbeat Timeout Too Short
```python
# BAD - Heartbeat timeout shorter than processing time
await workflow.execute_activity(
process_chunk,
start_to_close_timeout=timedelta(minutes=30),
heartbeat_timeout=timedelta(seconds=10), # Too short!
)
# GOOD - Heartbeat timeout allows for processing variance
await workflow.execute_activity(
process_chunk,
start_to_close_timeout=timedelta(minutes=30),
heartbeat_timeout=timedelta(minutes=2),
)
```
Set heartbeat timeout as high as acceptable for your use case — each heartbeat counts as an action.
## Cancellation
### Not Handling Workflow Cancellation
```python
# BAD - Cleanup doesn't run on cancellation
@workflow.defn
class BadWorkflow:
@workflow.run
async def run(self) -> None:
await workflow.execute_activity(
acquire_resource,
start_to_close_timeout=timedelta(minutes=5),
)
await workflow.execute_activity(
do_work,
start_to_close_timeout=timedelta(minutes=5),
)
await workflow.execute_activity(
release_resource, # Never runs if cancelled!
start_to_close_timeout=timedelta(minutes=5),
)
# GOOD - Use try/finally for cleanup
@workflow.defn
class GoodWorkflow:
@workflow.run
async def run(self) -> None:
await workflow.execute_activity(
acquire_resource,
start_to_close_timeout=timedelta(minutes=5),
)
try:
await workflow.execute_activity(
do_work,
start_to_close_timeout=timedelta(minutes=5),
)
finally:
# Runs even on cancellation
await workflow.execute_activity(
release_resource,
start_to_close_timeout=timedelta(minutes=5),
)
```
### Not Handling Activity Cancellation
Activities must **opt in** to receive cancellation. This requires:
1. **Heartbeating** - Cancellation is delivered via heartbeat
2. **Catching the cancellation exception** - Exception is raised when heartbeat detects cancellation
**Cancellation exceptions:**
- Async activities: `asyncio.CancelledError`
- Sync threaded activities: `temporalio.exceptions.CancelledError`
```python
# BAD - Activity ignores cancellation
@activity.defn
async def long_activity() -> None:
await do_expensive_work() # Runs to completion even if cancelled
```
```python
# GOOD - Heartbeat and catch cancellation
@activity.defn
async def long_activity() -> None:
try:
for item in items:
activity.heartbeat()
await process(item)
except asyncio.CancelledError:
await cleanup()
raise
```
## Testing
### Not Testing Failures
It is important to make sure workflows work as expected under failure paths in addition to happy paths. Please see `references/python/testing.md` for more info.
### Not Testing Replay
Replay tests help you test that you do not have hidden sources of non-determinism bugs in your workflow code, and should be considered in addition to standard testing. Please see `references/python/testing.md` for more info.
## Timers and Sleep
### Using asyncio.sleep
```python
# BAD: asyncio.sleep is not deterministic during replay
import asyncio
@workflow.defn
class BadWorkflow:
@workflow.run
async def run(self) -> None:
await asyncio.sleep(60) # Non-deterministic!
```
```python
# GOOD: Use workflow.sleep for deterministic timers
from temporalio import workflow
from datetime import timedelta
@workflow.defn
class GoodWorkflow:
@workflow.run
async def run(self) -> None:
await workflow.sleep(timedelta(seconds=60)) # Deterministic
# Or with string duration:
await workflow.sleep("1 minute")
```
**Why this matters:** `asyncio.sleep` uses the system clock, which differs between original execution and replay. `workflow.sleep` creates a durable timer in the event history, ensuring consistent behavior during replay.