temporalint
Static checks for Temporal Python SDK code. Temporal's workflow APIs fail at runtime for mistakes Python will not catch: a missing activity timeout, a discarded coroutine, a workflow class the worker will reject, or a nondeterministic call that breaks replay.
temporalint reports only patterns that are unambiguous. If it cannot resolve a call (a star import, a dot import, **kwargs, or a helper defined outside the workflow), it stays quiet.
The linter uses the standard library only. It does not import temporalio and does not run the code it checks.
Install
pip install temporalint
Usage
temporalint [paths...] [--select TPL001,TPL002] [--ignore TPL005]
Output is one finding per line:
workflow.py:14:5: TPL001 execute_activity sets neither start_to_close_timeout nor schedule_to_close_timeout
Rules
| Code | Name | What it flags |
|---|---|---|
| TPL001 | activity-timeout | execute_activity, start_activity, execute_local_activity, start_local_activity, and their _method / _class variants, when neither start_to_close_timeout nor schedule_to_close_timeout is passed. The SDK raises ValueError at runtime. Calls that pass **kwargs are skipped, because the timeout may be in that mapping. |
| TPL002 | missing-await | A bare statement that calls execute_activity*, execute_local_activity*, execute_child_workflow, start_child_workflow, sleep, or wait_condition without await. The coroutine is discarded. start_activity* is not included. Assigning the call (handle = workflow.execute_activity(...)) is not reported. |
| TPL003 | workflow-defn-shape | A @workflow.defn class with zero @workflow.run methods, more than one, or a @workflow.run method that is not async def. The worker rejects these at registration. Decorators may be called (@workflow.defn(name="...")) or imported by name (@defn). |
| TPL004 | nondeterministic-call | Inside a @workflow.defn class: datetime.now / utcnow / today, date.today, time.time / time_ns / monotonic / perf_counter, any random.* call, uuid.uuid1 / uuid.uuid4, os.urandom, and any secrets.* call. Use workflow.now(), workflow.random(), or workflow.uuid4(). |
| TPL005 | workflow-logger | Inside a @workflow.defn class: print(...), logging.debug / info / warning / warn / error / critical / exception / log / fatal, and the same methods on a direct logging.getLogger(...).info(...) call. Use workflow.logger. A logger stored in a variable is not resolved. |
| TPL015 | arg-and-args | execute_activity*, start_activity*, execute_local_activity*, start_local_activity*, execute_child_workflow, or start_child_workflow passes both a single arg and a non-empty args sequence. The SDK raises ValueError. An empty literal args is valid. A non-literal args may be empty, so it is skipped. **kwargs skips the call. |
| TPL018 | workflow-exception | Inside a @workflow.defn class, a raise of an exception that does not extend temporalio.exceptions.FailureError, or an assert. The workflow task fails and retries until the execution timeout, which is unlimited by default, so the execution never fails. Raise ApplicationError to fail the execution. An imported type whose qualified name resolved, and that is not a Temporal failure or control-flow exception, is flagged. A same-module class is included when its bases are known and do not include a Temporal failure type, including self.MyError() or Workflow.MyError() for a class defined on the workflow. Not flagged: a @workflow.query method, an @update.validator (any exception rejects the update), a bare raise, a raise of a variable, a name that does not resolve, a same-module class with an unknown base, ContinueAsNewError, and asyncio.CancelledError. Also not flagged when @workflow.defn(failure_exception_types=...) lists that type or a base of it, including Exception. A type listed only on the worker's workflow_failure_exception_types is not visible and is still flagged. |
Workflow scope for TPL004 and TPL005 is the body of a @workflow.defn class, including nested functions and lambdas. Calls inside helpers defined outside that class are not inspected.
Candidate rules
These are not implemented. Each one is a pattern the SDK rejects, or a default sandbox restriction, that is visible in the AST. A non-literal name, **kwargs, an unresolved import, or a helper defined outside the workflow class stays quiet.
Extensions
| Rule | Also flag |
|---|---|
| TPL002 | A bare workflow.wait(...). A bare workflow.create_nexus_client(...).execute_operation(...) or .start_operation(...). A Nexus client stored in a variable is not resolved. continue_as_new is not a coroutine. |
| TPL004 | Inside a @workflow.defn class, the remaining default sandbox restrictions: time.sleep, time.monotonic_ns, time.perf_counter_ns, time.process_time, time.process_time_ns, time.thread_time, time.thread_time_ns, time.localtime, time.tzset, and time.get_clock_info; any os call other than os.stat and os.path; open(...) and input(...); asyncio.wait and asyncio.as_completed (use workflow.wait and workflow.as_completed); any call on threading, multiprocessing, subprocess, socket, tempfile, glob, locale, platform, concurrent.futures, http.client, http.server, or urllib.request; and the filesystem methods on pathlib.Path (cwd, home, exists, is_file, is_dir, iterdir, glob, rglob, resolve, stat, mkdir, open, read_text, read_bytes, write_text, write_bytes, unlink, rename, and the other methods in the default sandbox matcher). random.Random(...) and secrets.compare_digest are allowed by the sandbox, so they are not candidates. |
New rules
| Code | Name | What it would flag |
|---|---|---|
| TPL006 | activity-keyword-only | An @activity.defn function with a keyword-only parameter. The SDK raises TypeError when the decorator runs. |
| TPL007 | dynamic-activity-signature | @activity.defn(dynamic=True) whose callable does not take exactly one argument annotated Sequence[temporalio.common.RawValue] (self plus that argument on a method). The SDK raises TypeError. Skip the check when dynamic is not a literal. |
| TPL008 | workflow-init | @workflow.init on a method other than __init__, or an __init__ whose parameter list differs from @workflow.run in kind, order, defaults, or annotations. Parameter names are ignored. The SDK raises ValueError. |
| TPL009 | duplicate-handler | Two @workflow.signal, @workflow.query, or @workflow.update methods on one class with the same name. The name is the name string literal, or the method name when name is omitted. A second dynamic=True handler is a duplicate. Also flag a decorator call that passes both name and dynamic: the SDK raises RuntimeError. Skip a handler whose name is not a literal. |
| TPL010 | dynamic-handler-signature | @workflow.defn(dynamic=True) whose @workflow.run does not take a single Sequence[temporalio.common.RawValue]. A dynamic=True signal, query, or update whose parameters are not (self, name: str, args: Sequence[temporalio.common.RawValue]). A dynamic update that takes *args instead. The SDK raises TypeError or RuntimeError. The older *args form on a dynamic signal or query is only a deprecation warning, so it is not included. |
| TPL011 | dynamic-config | @workflow.dynamic_config that is async def, takes more than self, appears twice, or is used on a class that is not @workflow.defn(dynamic=True). The SDK raises ValueError. |
| TPL012 | local-workflow-class | @workflow.defn on a class defined inside a function. The SDK raises ValueError because the class cannot be named for replay. |
| TPL013 | handler-override | A method that overrides a same-module base method decorated with @workflow.signal, @workflow.query, or @workflow.update, and the override omits that decorator. The SDK raises ValueError. An imported base is not inspected. A missing @workflow.run on the subclass is already TPL003. |
| TPL014 | duplicate-registration-name | Two @workflow.defn classes, or two @activity.defn callables, in the same module with the same registration name. The name is the name string literal, or the class or function name when name is omitted. dynamic=True has no name and is not part of this check. |
| TPL016 | query-workflow-call | Inside a @workflow.query method: execute_activity*, start_activity*, execute_local_activity*, start_local_activity*, execute_child_workflow, start_child_workflow, sleep, wait_condition, or wait. A query cannot schedule work or wait. workflow.now(), workflow.random(), and reads of workflow state are fine. An async def query with none of these calls is only a deprecation warning, so the async def itself is not flagged. |
| TPL017 | update-validator | An @update.validator method that is async def, whose parameter list differs from the update handler, or a second validator on the same update. The validator runs synchronously, must return None, and must match the handler's parameters apart from names. |
Configuration
Settings can live in temporalint.toml, .temporalint.toml, or pyproject.toml. Discovery starts at the working directory and walks toward the filesystem root. In each directory, temporalint.toml wins over .temporalint.toml, which wins over pyproject.toml. Every rule is enabled when select and ignore are omitted.
temporalint.toml and .temporalint.toml put the keys at the top level:
select = ["TPL001", "TPL002", "TPL003", "TPL004", "TPL005", "TPL015", "TPL018"]
ignore = ["TPL005"]
exclude = ["tests/**", "**/migrations/**"]
The same settings in pyproject.toml sit under [tool.temporalint]:
[tool.temporalint]
select = ["TPL001", "TPL002", "TPL003", "TPL004", "TPL005", "TPL015", "TPL018"]
ignore = ["TPL005"]
exclude = ["tests/**", "**/migrations/**"]
--select and --ignore replace the corresponding config values when you pass them. exclude is a list of glob patterns matched against the path relative to the config file's directory. An explicit file argument is still linted when it matches an exclude pattern.
Limitations
- Helper functions called from a workflow are not analyzed. Keep determinism-sensitive code in the workflow class, or review helpers separately.
from temporalio.workflow import *and relative imports are not resolved.- The timeout check looks for the keyword, not the value.
start_to_close_timeout=Noneis not reported, and**kwargssuppresses TPL001. - Argument counts, payload types, and activity names passed as strings are not checked yet.
- TPL018 cannot see
workflow_failure_exception_typeson the worker. A type allowed only there is still reported.
Metadata
Release files for temporalint 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| temporalint-0.1.0.tar.gz | 16.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| temporalint-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 34.7 kB
Release files / temporalint-0.1.0.tar.gz
| Download URL | temporalint-0.1.0.tar.gz |
|---|---|
| Size | 16.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
94f6204cef177d118c35d6f79d2afb0a569f248a614e00b0b18f430d2a0ccb89
|
|
BLAKE2b-256 checksum How to use checksums |
2d128fe27f601ac4ade9dd666ba9ca10bcfd2f341a978214d69b773b5033262e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency logRelease files / temporalint-0.1.0-py3-none-any.whl
| Download URL | temporalint-0.1.0-py3-none-any.whl |
|---|---|
| Size | 18.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
72b60305a76ef0156d40a8aca1fc06b3d2177ff4a548c0770bbcd86ee7100725
|
|
BLAKE2b-256 checksum How to use checksums |
bf1e67424caf4bf87448a3e6970545d5903b58a7f2b5fff922cb87fc7d22514a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency log