|
| 1 | +--- |
| 2 | +id: upgrading-to-v3 |
| 3 | +title: Upgrading to v3 |
| 4 | +description: Breaking changes and migration guide from v2 to v3. |
| 5 | +--- |
| 6 | + |
| 7 | +import ApiLink from '@site/src/components/ApiLink'; |
| 8 | + |
| 9 | +This page summarizes the breaking changes between Apify Python API Client v2.x and v3.0. |
| 10 | + |
| 11 | +## Python version support |
| 12 | + |
| 13 | +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. |
| 14 | + |
| 15 | +## Fully typed clients |
| 16 | + |
| 17 | +Resource client methods now return [Pydantic](https://docs.pydantic.dev/latest/) models instead of plain dictionaries. This provides IDE autocompletion, type checking, and early validation of API responses. |
| 18 | + |
| 19 | +### Accessing response fields |
| 20 | + |
| 21 | +Before (v2): |
| 22 | + |
| 23 | +```python |
| 24 | +from apify_client import ApifyClient |
| 25 | + |
| 26 | +client = ApifyClient(token='MY-APIFY-TOKEN') |
| 27 | + |
| 28 | +# v2 — methods returned plain dicts |
| 29 | +run = client.actor('apify/hello-world').call(run_input={'key': 'value'}) |
| 30 | +dataset_id = run['defaultDatasetId'] |
| 31 | +status = run['status'] |
| 32 | +``` |
| 33 | + |
| 34 | +After (v3): |
| 35 | + |
| 36 | +```python |
| 37 | +from apify_client import ApifyClient |
| 38 | + |
| 39 | +client = ApifyClient(token='MY-APIFY-TOKEN') |
| 40 | + |
| 41 | +# v3 — methods return Pydantic models |
| 42 | +run = client.actor('apify/hello-world').call(run_input={'key': 'value'}) |
| 43 | +dataset_id = run.default_dataset_id |
| 44 | +status = run.status |
| 45 | +``` |
| 46 | + |
| 47 | +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: |
| 48 | + |
| 49 | +```python |
| 50 | +run.default_dataset_id # ✓ use snake_case attribute names |
| 51 | +run.id |
| 52 | +run.status |
| 53 | +``` |
| 54 | + |
| 55 | +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: |
| 56 | + |
| 57 | +```python |
| 58 | +from apify_client._models import Run |
| 59 | + |
| 60 | +# Both work when constructing models |
| 61 | +Run(default_dataset_id='abc') # Python field name |
| 62 | +Run(defaultDatasetId='abc') # camelCase API alias |
| 63 | +``` |
| 64 | + |
| 65 | +### Exceptions |
| 66 | + |
| 67 | +Not every method returns a Pydantic model. Methods whose payloads are user-defined or inherently unstructured still return plain types: |
| 68 | + |
| 69 | +- <ApiLink to="class/DatasetClient#list_items">`DatasetClient.list_items()`</ApiLink> returns `DatasetItemsPage`, a dataclass whose `items` field is `list[dict[str, Any]]`, because the structure of dataset items is defined by the [Actor output schema](https://docs.apify.com/platform/actors/development/actor-definition/output-schema), which the API Client or SDK has no knowledge of. |
| 70 | +- <ApiLink to="class/KeyValueStoreClient#get_record">`KeyValueStoreClient.get_record()`</ApiLink> returns a `dict` with `key`, `value`, and `content_type` keys. |
| 71 | + |
| 72 | +### Pydantic models as method parameters |
| 73 | + |
| 74 | +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. |
| 75 | + |
| 76 | +Before (v2): |
| 77 | + |
| 78 | +```python |
| 79 | +rq_client.add_request({ |
| 80 | + 'url': 'https://example.com', |
| 81 | + 'uniqueKey': 'https://example.com', |
| 82 | + 'method': 'GET', |
| 83 | +}) |
| 84 | +``` |
| 85 | + |
| 86 | +After (v3) — both forms are accepted: |
| 87 | + |
| 88 | +```python |
| 89 | +from apify_client._types import RequestInput |
| 90 | + |
| 91 | +# Option 1: dict (still works) |
| 92 | +rq_client.add_request({ |
| 93 | + 'url': 'https://example.com', |
| 94 | + 'uniqueKey': 'https://example.com', |
| 95 | + 'method': 'GET', |
| 96 | +}) |
| 97 | + |
| 98 | +# Option 2: Pydantic model (new) |
| 99 | +rq_client.add_request(RequestInput( |
| 100 | + url='https://example.com', |
| 101 | + unique_key='https://example.com', |
| 102 | + method='GET', |
| 103 | +)) |
| 104 | +``` |
| 105 | + |
| 106 | +Model input is available on methods such as <ApiLink to="class/RequestQueueClient#add_request">`RequestQueueClient.add_request()`</ApiLink>, <ApiLink to="class/RequestQueueClient#batch_add_requests">`RequestQueueClient.batch_add_requests()`</ApiLink>, <ApiLink to="class/ActorClient#start">`ActorClient.start()`</ApiLink>, <ApiLink to="class/ActorClient#call">`ActorClient.call()`</ApiLink>, <ApiLink to="class/TaskClient#start">`TaskClient.start()`</ApiLink>, <ApiLink to="class/TaskClient#call">`TaskClient.call()`</ApiLink>, <ApiLink to="class/TaskClient#update">`TaskClient.update()`</ApiLink>, and <ApiLink to="class/TaskClient#update_input">`TaskClient.update_input()`</ApiLink>, among others. Check the API reference for the complete list. |
| 107 | + |
| 108 | +## Pluggable HTTP client architecture |
| 109 | + |
| 110 | +The HTTP layer is now abstracted behind <ApiLink to="class/HttpClient">`HttpClient`</ApiLink> and <ApiLink to="class/HttpClientAsync">`HttpClientAsync`</ApiLink> base classes. The default implementation based on [Impit](https://github.com/apify/impit) (<ApiLink to="class/ImpitHttpClient">`ImpitHttpClient`</ApiLink> / <ApiLink to="class/ImpitHttpClientAsync">`ImpitHttpClientAsync`</ApiLink>) is unchanged, but you can now replace it with your own. |
| 111 | + |
| 112 | +To use a custom HTTP client, implement the `call()` method and pass the instance via the <ApiLink to="class/ApifyClient#with_custom_http_client">`ApifyClient.with_custom_http_client()`</ApiLink> class method: |
| 113 | + |
| 114 | +```python |
| 115 | +from apify_client import ApifyClient, HttpClient, HttpResponse, Timeout |
| 116 | + |
| 117 | +class MyHttpClient(HttpClient): |
| 118 | + def call(self, *, method, url, headers=None, params=None, |
| 119 | + data=None, json=None, stream=None, timeout='medium') -> HttpResponse: |
| 120 | + ... |
| 121 | + |
| 122 | +client = ApifyClient.with_custom_http_client( |
| 123 | + token='MY-APIFY-TOKEN', |
| 124 | + http_client=MyHttpClient(), |
| 125 | +) |
| 126 | +``` |
| 127 | + |
| 128 | +The response must satisfy the <ApiLink to="class/HttpResponse">`HttpResponse`</ApiLink> 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. |
| 129 | + |
| 130 | +For a full walkthrough and working examples, see the [Custom HTTP clients](/docs/concepts/custom-http-clients) concept page and the [Custom HTTP client](/docs/guides/custom-http-client-httpx) guide. |
| 131 | + |
| 132 | +## Tiered timeout system |
| 133 | + |
| 134 | +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. |
| 135 | + |
| 136 | +### Timeout tiers |
| 137 | + |
| 138 | +| Tier | Default | Typical use case | |
| 139 | +|---|---|---| |
| 140 | +| `short` | 5 s | Fast CRUD operations (get, update, delete) | |
| 141 | +| `medium` | 30 s | Batch and list operations, starting runs | |
| 142 | +| `long` | 360 s | Long-polling, streaming, data retrieval | |
| 143 | +| `no_timeout` | Disabled | Blocking calls like `actor.call()` that wait for a run to finish | |
| 144 | + |
| 145 | +A `timeout_max` value (default 360 s) caps the exponential growth of timeouts across retries. |
| 146 | + |
| 147 | +### Configuring default tiers |
| 148 | + |
| 149 | +You can override the default duration of any tier on the <ApiLink to="class/ApifyClient">`ApifyClient`</ApiLink> constructor: |
| 150 | + |
| 151 | +```python |
| 152 | +from datetime import timedelta |
| 153 | + |
| 154 | +from apify_client import ApifyClient |
| 155 | + |
| 156 | +client = ApifyClient( |
| 157 | + token='MY-APIFY-TOKEN', |
| 158 | + timeout_short=timedelta(seconds=10), |
| 159 | + timeout_medium=timedelta(seconds=60), |
| 160 | + timeout_long=timedelta(seconds=600), |
| 161 | + timeout_max=timedelta(seconds=600), |
| 162 | +) |
| 163 | +``` |
| 164 | + |
| 165 | +### Per-call override |
| 166 | + |
| 167 | +Every resource client method exposes a `timeout` parameter. You can pass a tier name or a `timedelta` for a one-off override: |
| 168 | + |
| 169 | +```python |
| 170 | +from datetime import timedelta |
| 171 | + |
| 172 | +# Use the 'long' tier for this specific call |
| 173 | +actor = client.actor('apify/hello-world').get(timeout='long') |
| 174 | + |
| 175 | +# Or pass an explicit duration |
| 176 | +actor = client.actor('apify/hello-world').get(timeout=timedelta(seconds=120)) |
| 177 | +``` |
| 178 | + |
| 179 | +### Retry behavior |
| 180 | + |
| 181 | +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. |
| 182 | + |
| 183 | +### Updated default timeout tiers |
| 184 | + |
| 185 | +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`. |
| 186 | + |
| 187 | +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 <ApiLink to="class/ApifyClient">`ApifyClient`</ApiLink> constructor (see [Tiered timeout system](#tiered-timeout-system) above). |
0 commit comments