Typed models
Resource client methods return Pydantic models generated directly from the Apify OpenAPI specification. You get IDE autocompletion, runtime validation of API responses, and a Python-idiomatic snake_case interface on top of the underlying camelCase API.
Accessing response fields
Every method that returns a structured payload returns a Pydantic model. Fields are accessed using their Python snake_case names regardless of the camelCase used by the API, and the static type of each field comes through to your editor.
- Async client
- Sync client
from apify_client import ApifyClientAsync
TOKEN = 'MY-APIFY-TOKEN'
async def main() -> None:
apify_client = ApifyClientAsync(TOKEN)
# `get` returns an `Actor` Pydantic model — fields are typed and IDE-completable.
actor = await apify_client.actor('apify/hello-world').get()
if actor is None:
return
print(actor.id) # str
print(actor.username) # str
print(actor.is_public) # bool
print(actor.created_at) # datetime.datetime (timezone-aware)
print(actor.stats.total_runs) # int — nested model, attribute access all the way down
from apify_client import ApifyClient
TOKEN = 'MY-APIFY-TOKEN'
def main() -> None:
apify_client = ApifyClient(TOKEN)
# `get` returns an `Actor` Pydantic model — fields are typed and IDE-completable.
actor = apify_client.actor('apify/hello-world').get()
if actor is None:
return
print(actor.id) # str
print(actor.username) # str
print(actor.is_public) # bool
print(actor.created_at) # datetime.datetime (timezone-aware)
print(actor.stats.total_runs) # int — nested model, attribute access all the way down
Date strings are automatically parsed into timezone-aware datetime.datetime objects, enums into Literal aliases, and nested objects into their own typed models, so you can compose attribute access without manual conversion.
URL fields
Fields holding a URL, such as Run.container_url or Dataset.console_url, are typed as AnyUrl. Their values are validated and normalized, so the string you read back can differ from the one the API sent. Both forms denote the same URL under RFC 3986, they're just different strings.
from pydantic import AnyUrl
# An empty path becomes '/'.
str(AnyUrl('https://abc123.runs.apify.net')) # 'https://abc123.runs.apify.net/'
# The host is lowercased.
str(AnyUrl('https://EXAMPLE.com/Path')) # 'https://example.com/Path'
# A default port is dropped.
str(AnyUrl('https://example.com:443/path')) # 'https://example.com/path'
# An internationalized host is punycoded.
str(AnyUrl('https://www.žluty.cz')) # 'https://www.xn--luty-kbb.cz/'
# Unsafe characters are percent-encoded.
str(AnyUrl('https://example.com/a b')) # 'https://example.com/a%20b'
Because of the normalization, compare URLs in their normalized form instead of as raw strings. AnyUrl compares the parsed URL, not the input text.
run = client.run('my-run-id').get()
stored_url = 'https://abc123.runs.apify.net'
# Wrong, compares a raw string with a normalized one.
stored_url == str(run.container_url) # False
# Right, both sides are normalized before the comparison.
AnyUrl(stored_url) == run.container_url # True
To build a longer URL out of a URL field, use urljoin rather than string concatenation. It gives the same result whether or not the base ends with a slash.
from urllib.parse import urljoin
urljoin(str(run.container_url), 'status') # 'https://abc123.runs.apify.net/status'
Providing structured input
A Pydantic model returned from one client call can be passed directly into any other method that accepts the same shape — useful for round-trip flows where you read a resource, tweak it, and write it back.
For input you construct yourself, plain dictionaries work on every input method. Each input shape has a matching TypedDict that documents the expected keys.
- Async client
- Sync client
from apify_client import ApifyClientAsync
TOKEN = 'MY-APIFY-TOKEN'
async def main() -> None:
apify_client = ApifyClientAsync(TOKEN)
rq_client = apify_client.request_queue('REQUEST-QUEUE-ID')
# Plain dict — keys may be snake_case or camelCase.
await rq_client.add_request(
{
'url': 'https://example.com',
'unique_key': 'https://example.com',
'method': 'GET',
}
)
from apify_client import ApifyClient
TOKEN = 'MY-APIFY-TOKEN'
def main() -> None:
apify_client = ApifyClient(TOKEN)
rq_client = apify_client.request_queue('REQUEST-QUEUE-ID')
# Plain dict — keys may be snake_case or camelCase.
rq_client.add_request(
{
'url': 'https://example.com',
'unique_key': 'https://example.com',
'method': 'GET',
}
)
Forward compatibility
Generated models are configured with extra='allow'. Any new fields the API starts returning in the future are preserved on the model instance — they simply do not yet have a typed attribute. Upgrading the client to pick up a newer OpenAPI spec is a non-breaking change for code that reads existing fields.
Browsing all models
The full list of generated models and TypedDicts is available in the API reference under the Models and Typed dicts groups.
Methods that return plain types
A few endpoints intentionally return plain Python types instead of Pydantic models because their payloads are user-defined or inherently unstructured:
DatasetClient.list_items()returns aDatasetItemsPagewhoseitemsfield islist[dict[str, Any]]. Dataset items follow the Actor output schema, which the client cannot know in advance.KeyValueStoreClient.get_record()returns adictwithkey,value, andcontent_typekeys. The shape ofvalueis determined by the record's content type.
For background on the migration from plain dicts to typed models, see Upgrading to v3.