Upgrading to v3
This page summarizes the breaking changes between Apify Python API Client v2.x and v3.0.
Python version support
Support for Python 3.10 has been dropped. The Apify Python API Client v3.x now requires Python 3.11 or later. Make sure your environment is running a compatible version before upgrading.
Fully typed clients
Resource client methods now return Pydantic models instead of plain dictionaries. This provides IDE autocompletion, type checking, and early validation of API responses.
Accessing response fields
Before (v2):
from apify_client import ApifyClient
client = ApifyClient(token='MY-APIFY-TOKEN')
# v2 — methods returned plain dicts
run = client.actor('apify/hello-world').call(run_input={'key': 'value'})
dataset_id = run['defaultDatasetId']
status = run['status']
After (v3):
from apify_client import ApifyClient
client = ApifyClient(token='MY-APIFY-TOKEN')
# v3 — methods return Pydantic models
run = client.actor('apify/hello-world').call(run_input={'key': 'value'})
dataset_id = run.default_dataset_id
status = run.status
All model classes are generated from the Apify OpenAPI specification and live in apify_client._models module. They are configured with extra='allow', so any new fields added to the API in the future are preserved on the model instance. Fields are accessed using their Python snake_case names:
run.default_dataset_id # ✓ use snake_case attribute names
run.id
run.status
Models also use populate_by_name=True, which means you can use either the Python field name or the camelCase alias when constructing a model:
from apify_client._models_generated import Run
# Both work when constructing models
Run(default_dataset_id='abc') # Python field name
Run(defaultDatasetId='abc') # camelCase API alias
Exceptions
Not every method returns a Pydantic model. Methods whose payloads are user-defined or inherently unstructured still return plain types:
DatasetClient.list_items()returnsDatasetItemsPage, a dataclass whoseitemsfield islist[dict[str, Any]], because the structure of dataset items is defined by the Actor output schema, which the API Client or SDK has no knowledge of.KeyValueStoreClient.get_record()returns adictwithkey,value, andcontent_typekeys.
Pydantic models as method parameters
Resource client methods that previously accepted only dictionaries for structured input now also accept Pydantic models. Existing code that passes dictionaries continues to work — this change is additive for callers, but is listed here because method type signatures have changed.
Before (v2):
rq_client.add_request({
'url': 'https://example.com',
'uniqueKey': 'https://example.com',
'method': 'GET',
})
After (v3) — both forms are accepted:
from apify_client._models import RequestInput
# Option 1: dict (still works)
rq_client.add_request({
'url': 'https://example.com',
'uniqueKey': 'https://example.com',
'method': 'GET',
})
# Option 2: Pydantic model (new)
rq_client.add_request(RequestInput(
url='https://example.com',
unique_key='https://example.com',
method='GET',
))
Model input is available on methods such as RequestQueueClient.add_request(), RequestQueueClient.batch_add_requests(), ActorClient.start(), ActorClient.call(), TaskClient.start(), TaskClient.call(), TaskClient.update(), and TaskClient.update_input(), among others. Check the API reference for the complete list.
Pluggable HTTP client architecture
The HTTP layer is now abstracted behind HttpClient and HttpClientAsync base classes. The default implementation based on Impit (ImpitHttpClient / ImpitHttpClientAsync) is unchanged, but you can now replace it with your own.
To use a custom HTTP client, implement the call() method and pass the instance via the ApifyClient.with_custom_http_client() class method:
from apify_client import ApifyClient, HttpClient, HttpResponse, Timeout
class MyHttpClient(HttpClient):
def call(self, *, method, url, headers=None, params=None,
data=None, json=None, stream=None, timeout='medium') -> HttpResponse:
...
client = ApifyClient.with_custom_http_client(
token='MY-APIFY-TOKEN',
http_client=MyHttpClient(),
)
The response must satisfy the HttpResponse protocol (properties: status_code, text, content, headers; methods: json(), read(), close(), iter_bytes()). Many popular libraries like httpx already satisfy this protocol out of the box.
For a full walkthrough and working examples, see the Custom HTTP clients concept page and the Custom HTTP client guide.
Tiered timeout system
Individual API methods now use a tiered timeout instead of a single global timeout. Each method declares a default tier appropriate for its expected latency.
Timeout tiers
| Tier | Default | Typical use case |
|---|---|---|
short | 5 s | Fast CRUD operations (get, update, delete) |
medium | 30 s | Batch and list operations, starting runs |
long | 360 s | Long-polling, streaming, data retrieval |
no_timeout | Disabled | Blocking calls like actor.call() that wait for a run to finish |
A timeout_max value (default 360 s) caps the exponential growth of timeouts across retries.
Configuring default tiers
You can override the default duration of any tier on the ApifyClient constructor:
from datetime import timedelta
from apify_client import ApifyClient
client = ApifyClient(
token='MY-APIFY-TOKEN',
timeout_short=timedelta(seconds=10),
timeout_medium=timedelta(seconds=60),
timeout_long=timedelta(seconds=600),
timeout_max=timedelta(seconds=600),
)
Per-call override
Every resource client method exposes a timeout parameter. You can pass a tier name or a timedelta for a one-off override:
from datetime import timedelta
# Use the 'long' tier for this specific call
actor = client.actor('apify/hello-world').get(timeout='long')
# Or pass an explicit duration
actor = client.actor('apify/hello-world').get(timeout=timedelta(seconds=120))
Retry behavior
On retries, the timeout doubles with each attempt (exponential backoff) up to timeout_max. For example, with timeout_short=5s and timeout_max=360s: attempt 1 uses 5 s, attempt 2 uses 10 s, attempt 3 uses 20 s, and so on.
Updated default timeout tiers
The default timeout tier assigned to each method on non-storage resource clients has been revised to better match the expected latency of the underlying API endpoint. For example, a simple get() call now defaults to short (5 s), while start() defaults to medium (30 s) and call() defaults to no_timeout.
If your code relied on the previous global timeout behavior, review the timeout tier on the methods you use and adjust via the timeout parameter or by overriding tier defaults on the ApifyClient constructor (see Tiered timeout system above).
Exception subclasses for API errors
ApifyApiError now dispatches to a dedicated subclass based on the HTTP status code of the failed response. Instantiating ApifyApiError directly still works — it returns the most specific subclass for the status — so existing except ApifyApiError handlers are unaffected.
The following subclasses are available:
| Status | Subclass |
|---|---|
| 400 | InvalidRequestError |
| 401 | UnauthorizedError |
| 403 | ForbiddenError |
| 404 | NotFoundError |
| 409 | ConflictError |
| 429 | RateLimitError |
| 5xx | ServerError |
You can now branch on error kind without inspecting status_code or type:
from apify_client import ApifyClient
from apify_client.errors import NotFoundError, RateLimitError
client = ApifyClient(token='MY-APIFY-TOKEN')
try:
run = client.run('some-run-id').get()
except NotFoundError:
run = None
except RateLimitError:
...
Behavior change: 404 on ambiguous endpoints now raises NotFoundError
Direct, ID-identified fetches like client.dataset(id).get() or client.run(id).get() continue to swallow 404 into None — a 404 there unambiguously means the named resource does not exist. Similarly, .delete() on an ID-identified client keeps its idempotent behavior (404 is silently swallowed).
For calls where a 404 is ambiguous, the client now propagates NotFoundError instead of returning None / silently succeeding. Three categories of endpoints are affected:
- Chained calls that target a default sub-resource without an ID —
run.dataset(),run.key_value_store(),run.request_queue(),run.log(). A 404 here could mean the parent run is missing OR the default sub-resource is missing, and the API body does not disambiguate. Applies to both.get()and.delete(). .get()/.get_as_bytes()/.stream()on a chainedLogClient— e.g.run.log().get(). Directclient.log(build_or_run_id).get()still returnsNoneon 404.- Singleton sub-resource endpoints fetched via a fixed path —
ScheduleClient.get_log(),TaskClient.get_input(),DatasetClient.get_statistics(),UserClient.monthly_usage(),UserClient.limits(),WebhookClient.test(). These hit paths like/schedules/{id}/logor/actor-tasks/{id}/input, so a 404 effectively means the parent is missing. Return types moved fromT | NonetoT.
from apify_client import ApifyClient
from apify_client.errors import NotFoundError
client = ApifyClient(token='MY-APIFY-TOKEN')
try:
dataset = client.run('some-run-id').dataset().get()
except NotFoundError:
# Previously this returned `None`; now you must handle it explicitly.
dataset = None
try:
schedule_log = client.schedule('some-schedule-id').get_log()
except NotFoundError:
# `get_log()` previously returned `None` when the schedule was missing; now it raises.
schedule_log = None
Direct .get() also now swallows every 404 regardless of the error.type string in the response body (previously only record-not-found and record-or-token-not-found types were swallowed). If your code needs to distinguish between "resource missing" and "404 with an unexpected type", inspect .type on a caught NotFoundError from a non-.get() call path.
Snake_case sort_by values on actors().list()
The sort_by parameter of ActorCollectionClient.list() and ActorCollectionClientAsync.list() now accepts pythonic snake_case values instead of the raw camelCase values used by the API.
Before (v2):
client.actors().list(sort_by='createdAt')
client.actors().list(sort_by='stats.lastRunStartedAt')
After (v3):
client.actors().list(sort_by='created_at')
client.actors().list(sort_by='last_run_started_at')
The default value also changed from 'createdAt' to 'created_at' (behavior is unchanged). The client translates the snake_case value to the form expected by the API internally.