Python is the right tool for anything Airtable can't do natively: bulk data migrations between systems, custom reporting that ETLs data into a warehouse, integrations with internal databases, and ML pipelines that use Airtable as a labeling layer or training-data store.
This guide is the practical walkthrough for using Python with Airtable. All examples use pyairtable — the de facto community library, well-maintained and complete.
Step 1: Install pyairtable
pip install pyairtable
Requires Python 3.8+. For production use, pin to a specific version in your requirements.txt:
pyairtable==3.0.1
Step 2: Set Up Authentication
Always use Personal Access Tokens (PATs), never legacy API keys (they stopped working in 2024). See our PAT guide for the setup.
Store the token in an environment variable, never in source code:
export AIRTABLE_TOKEN="pat_xxxxxxxx.yyyyyyyy"
Then in Python:
import os
from pyairtable import Api
api = Api(os.environ['AIRTABLE_TOKEN'])
For the Base ID and Table ID, see our find Airtable IDs guide.
Step 3: Read Records
Get all records (auto-paginated)
table = api.table('appXXXX', 'Tasks')
records = table.all()
for record in records:
print(record['fields']['Name'])
table.all() returns a list. For large tables, prefer iteration:
for record in table.iterate():
# process one page at a time without holding everything in memory
for r in record:
print(r['id'], r['fields'].get('Name'))
Filter with formulas
records = table.all(formula="AND({Status}='Open', {Priority}='High')")
The formula is any valid Airtable formula. See our formulas cheat sheet.
Scope to a view
records = table.all(view='My Open Tasks')
Limit returned fields
records = table.all(fields=['Name', 'Status', 'Due Date'])
Useful for large tables where you only need a few columns — reduces payload size and API time.
Sort and limit
records = table.all(
sort=['Due Date'], # ascending; prefix with '-' for descending
max_records=100
)
Get a single record by ID
record = table.get('recXXXXXXXXXXXXXX')
Step 4: Create Records
Single record
new_record = table.create({
'Name': 'New task',
'Status': 'Open',
'Priority': 'High',
'Due Date': '2026-07-15'
})
print(new_record['id']) # the new record's ID
Batch create
records_to_create = [
{'Name': 'Task 1', 'Status': 'Open'},
{'Name': 'Task 2', 'Status': 'Open'},
# ... up to thousands of records
]
created = table.batch_create(records_to_create)
pyairtable splits this into Airtable's 10-records-per-request limit automatically.
Field type formats
| Field Type | Python Format |
|---|---|
| Single line / Long text | "text value" |
| Number / Currency / Percent | 123.45 |
| Date | "2026-07-15" |
| Date and time | "2026-07-15T14:00:00.000Z" |
| Single select | "Option Name" |
| Multi-select | ["Option 1", "Option 2"] |
| Linked record | ["recXXXX", "recYYYY"] |
| Checkbox | True or False |
| Attachments | [{"url": "https://..."}] |
Step 5: Update Records
Single update (partial — only specified fields change)
table.update('recXXXX', {'Status': 'Done'})
Batch update
updates = [
{'id': 'recAAAA', 'fields': {'Status': 'Done'}},
{'id': 'recBBBB', 'fields': {'Status': 'Done'}},
]
table.batch_update(updates)
Replace (clears unspecified fields)
table.update('recXXXX', {'Status': 'Done'}, replace=True)
Use sparingly — partial updates are almost always what you want.
Step 6: Upsert (Create or Update)
pyairtable supports Airtable's native upsert endpoint:
table.batch_upsert(
records=[
{'fields': {'External ID': 'ABC123', 'Name': 'Hello'}},
{'fields': {'External ID': 'XYZ789', 'Name': 'World'}},
],
key_fields=['External ID']
)
If a record exists with the matching key field value, it's updated. Otherwise, it's created. This is the right pattern for any sync script.
Step 7: Delete Records
# Single delete
table.delete('recXXXX')
# Batch delete
table.batch_delete(['recAAAA', 'recBBBB', 'recCCCC'])
Step 8: Working with Attachments
Airtable's API accepts attachment URLs, not direct file uploads. The pattern:
- Upload the file to a public host (S3, Cloudinary, an HTTP server you control).
- Set the Airtable attachment field to an array of
{url: ...}objects.
table.update('recXXXX', {
'Document': [{'url': 'https://yourbucket.s3.amazonaws.com/file.pdf'}]
})
Airtable downloads the file and stores its own copy. The original URL can be torn down once Airtable has fetched it.
Reading attachments
record = table.get('recXXXX')
for attachment in record['fields'].get('Document', []):
print(attachment['url'], attachment['filename'], attachment['size'])
The URL in the API response points to Airtable's CDN. It rotates every few hours — don't hardcode it.
Step 9: Production Patterns
Rate limiting awareness
Airtable rate-limits at 5 requests per second per base. pyairtable retries 429 responses automatically, but for long-running scripts, add explicit pacing:
import time
for batch in chunks(big_list, 10):
table.batch_create(batch)
time.sleep(0.25) # 4 req/sec — under the limit
Idempotent upserts
For sync scripts that might be re-run after a partial failure, use upserts with a unique key. Re-running the same data is a no-op rather than a duplicate-creator.
Logging and metrics
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info(f"Synced {len(created)} new records, updated {len(updated)}")
For scheduled scripts, log to a service like Datadog or just stdout for cron logs.
Error handling
from pyairtable.api.types import RecordDict
from requests.exceptions import HTTPError
try:
record = table.create({...})
except HTTPError as e:
if e.response.status_code == 422:
logger.error(f"Invalid field values: {e.response.text}")
elif e.response.status_code == 429:
logger.error("Rate limited even after retries")
else:
raise
Environment configuration
For multi-environment scripts (dev/staging/prod), use separate Base IDs and PATs per environment loaded from .env files via python-dotenv:
from dotenv import load_dotenv
load_dotenv()
api = Api(os.environ['AIRTABLE_TOKEN'])
table = api.table(
os.environ['AIRTABLE_BASE_ID'],
os.environ['AIRTABLE_TABLE_NAME']
)
Step 10: Common Patterns
Sync from external API to Airtable
import requests
def sync_external_to_airtable():
external_data = requests.get('https://api.example.com/items').json()
upsert_payload = [
{'fields': {
'External ID': item['id'],
'Name': item['name'],
'Status': item['status'],
}}
for item in external_data
]
table.batch_upsert(upsert_payload, key_fields=['External ID'])
Export Airtable to CSV
import csv
records = table.all(fields=['Name', 'Status', 'Amount'])
with open('export.csv', 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['id', 'Name', 'Status', 'Amount'])
writer.writeheader()
for r in records:
writer.writerow({'id': r['id'], **r['fields']})
Bulk delete with confirmation
to_delete = table.all(formula="{Status}='Archived'")
ids = [r['id'] for r in to_delete]
confirm = input(f"Delete {len(ids)} records? (yes/no) ")
if confirm == 'yes':
table.batch_delete(ids)
Comparison: pyairtable vs Alternatives
| Library | Language | Pros | Cons |
|---|---|---|---|
| pyairtable | Python | Community-maintained, complete, idiomatic | Not officially supported |
| Official airtable-python | Python | Official | Deprecated, no longer maintained |
| airtable.js | JavaScript | Official | Different language |
| Raw HTTP via requests | Python | No dependencies | You write pagination, retries, batching |
For Python, pyairtable is the standard. The official Python library was deprecated years ago and isn't maintained.
Common Mistakes
Mistake 1: Hardcoding the PAT in code. Use environment variables. Commit a token to GitHub and it gets harvested within hours.
Mistake 2: Single-create loops for bulk operations. for r in records: table.create(r) takes 100x longer than table.batch_create(records). Always batch.
Mistake 3: Loading everything into memory. table.all() returns the full list. For tables with 100K+ rows, use table.iterate() and process page-by-page.
Mistake 4: Filtering client-side. Pulling 50K rows to filter in Python wastes time and rate limit. Use formula= server-side.
Mistake 5: Not handling the 429 case. pyairtable retries automatically, but if you're using raw HTTP, you must implement back-off yourself.
Troubleshooting
AuthorizationError. PAT is wrong, expired, or doesn't have the needed scope. Regenerate with data.records:read and/or data.records:write.
KeyError: 'fields'. A returned record had no fields (rare but happens for synced sources). Use .get('fields', {}) defensively.
Formula filter returns empty when records exist. Field names are case-sensitive, and curly braces are required for multi-word names: {Due Date} not Due Date.
Attachment update doesn't take. The URL isn't publicly accessible. Test by opening the URL in an incognito browser.
Script slow on large tables. You're probably not batching. Use batch_create / batch_update / batch_upsert instead of single-record calls.
Next Steps
Python opens up everything the Airtable UI can't do: data warehousing, custom ML pipelines, deep integration with internal systems, scheduled reports. Once you're comfortable with CRUD operations, the natural extensions are: scheduled scripts via cron or Airflow, integrations with internal databases, and ML pipelines using Airtable as a labeling layer.
For broader patterns, see our Airtable API beginner's guide, find Airtable IDs guide, PAT guide, scripting guide, and webhooks guide. For production data pipelines using Python and Airtable, get in touch.