š 2.8 Compare Common API Styles
Overview of API Styles
Different API architectural styles exist to serve different needs. The most common styles are REST and RPC, which can be implemented using either synchronous or asynchronous communication patterns.
1. REST (Representational State Transfer)
REST Resource-Oriented Architecture
REST is an architectural style that treats everything as a resource (users, products, orders, etc.). Resources are identified by URLs and manipulated using standard HTTP methods.
- Client-Server: Separation of concerns between client and server
- Stateless: Each request contains all needed information
- Cacheable: Responses can be cached for performance
- Uniform Interface: Standard HTTP methods and status codes
- Layered System: Can have intermediary layers (proxies, load balancers)
REST Characteristics
| Aspect | Description |
|---|---|
| Focus | Resources (nouns) - users, products, orders |
| Methods | HTTP verbs: GET, POST, PUT, PATCH, DELETE |
| URL Structure | /api/users/123 (resource-based) |
| Data Format | Usually JSON, can be XML, HTML |
| State | Stateless - no client context stored on server |
| Response | HTTP status codes (200, 404, 500, etc.) |
REST API Examples
# REST API Examples - Resource-based URLs with HTTP methods
# Get all users
GET /api/v1/users
# Get specific user
GET /api/v1/users/123
# Create new user
POST /api/v1/users
Body: {"name": "John Doe", "email": "john@example.com"}
# Update user (full replacement)
PUT /api/v1/users/123
Body: {"name": "John Smith", "email": "john.smith@example.com"}
# Update user (partial update)
PATCH /api/v1/users/123
Body: {"email": "newemail@example.com"}
# Delete user
DELETE /api/v1/users/123
# Nested resources (relationships)
GET /api/v1/users/123/orders # Get orders for user 123
GET /api/v1/users/123/orders/456 # Get specific order for user
# Filtering, sorting, pagination
GET /api/v1/users?role=admin&sort=created_at&page=2&limit=20- Scalable: Stateless design allows horizontal scaling
- Simple: Uses standard HTTP - easy to understand and implement
- Cacheable: Built-in HTTP caching mechanisms
- Flexible: Supports multiple data formats
- Widely Used: Industry standard, extensive tooling
- Over-fetching/Under-fetching: May get too much or too little data
- Multiple Requests: Complex operations may need multiple API calls
- No Standard Schema: API documentation varies widely
- Versioning: Can be challenging to maintain multiple versions
2. RPC (Remote Procedure Call)
RPC Action-Oriented Architecture
RPC (Remote Procedure Call) is an architectural style that treats API calls like function calls. Instead of resources, RPC focuses on actions/procedures to be executed.
- JSON-RPC: Uses JSON for encoding
- XML-RPC: Uses XML for encoding
- gRPC: Google's high-performance RPC framework using Protocol Buffers
RPC Characteristics
| Aspect | Description |
|---|---|
| Focus | Actions/procedures (verbs) - getUser, createOrder, calculateTotal |
| Methods | Usually POST for all operations |
| URL Structure | /api/getUser (action-based) or single endpoint |
| Data Format | JSON, XML, Protocol Buffers |
| State | Can be stateful or stateless |
| Response | Procedure-specific status in response body |
RPC API Examples
# JSON-RPC Example - All use POST to single endpoint
# Get user
POST /api/jsonrpc
Body: {
"jsonrpc": "2.0",
"method": "getUser",
"params": {"userId": 123},
"id": 1
}
# Create user
POST /api/jsonrpc
Body: {
"jsonrpc": "2.0",
"method": "createUser",
"params": {
"name": "John Doe",
"email": "john@example.com"
},
"id": 2
}
# Update user
POST /api/jsonrpc
Body: {
"jsonrpc": "2.0",
"method": "updateUser",
"params": {
"userId": 123,
"email": "newemail@example.com"
},
"id": 3
}
# Delete user
POST /api/jsonrpc
Body: {
"jsonrpc": "2.0",
"method": "deleteUser",
"params": {"userId": 123},
"id": 4
}
# Response format
{
"jsonrpc": "2.0",
"result": {
"userId": 123,
"name": "John Doe",
"email": "john@example.com"
},
"id": 1
}
# Error response
{
"jsonrpc": "2.0",
"error": {
"code": -32600,
"message": "Invalid Request"
},
"id": 1
}- Simple Concept: Like calling local functions
- Efficient: Lightweight protocol, less overhead
- Action-Oriented: Natural for command-based operations
- Flexible: Can bundle multiple operations in one call
- Strongly Typed: (gRPC) compile-time type checking
- Less Standard: No universal convention like REST
- Tight Coupling: Client needs to know exact procedure names
- Discovery: Harder to explore API without documentation
- Caching: Difficult to cache since everything is POST
- Not Resource-Oriented: Doesn't align with web architecture
3. Synchronous vs Asynchronous Communication
Synchronous Blocking Communication
Synchronous communication means the client sends a request and waits (blocks) until it receives a response from the server.
Synchronous Characteristics
- Blocking: Client waits for response before continuing
- Sequential: Operations happen one after another
- Simple: Easy to understand and implement
- Request-Response: Immediate response expected
- Example: Traditional REST API calls, database queries
# Synchronous Example - Python
import requests
# Client waits here until response received
response = requests.get('https://api.example.com/users/123')
# This line doesn't execute until response is received
print(f"User: {response.json()['name']}")
# Another synchronous call - waits again
response2 = requests.get('https://api.example.com/users/456')
print(f"User 2: {response2.json()['name']}")
# Total time = time1 + time2 (sequential)- Simple to understand and implement
- Easy to debug and trace execution
- Predictable execution order
- No callback complexity
- Blocks execution while waiting
- Inefficient for long-running operations
- Can't handle multiple operations concurrently
- Poor user experience in UI (freezes)
Asynchronous Non-Blocking Communication
Asynchronous communication means the client sends a request and continues executing without waiting. Response is handled via callback, promise, or event.
Asynchronous Characteristics
- Non-Blocking: Client continues execution immediately
- Concurrent: Multiple operations can run simultaneously
- Complex: Requires callbacks, promises, or async/await
- Event-Driven: Response triggers an event or callback
- Example: AJAX calls, WebSockets, message queues
# Asynchronous Example - Python with asyncio
import asyncio
import aiohttp
async def get_user(session, user_id):
"""Asynchronous function to get user"""
async with session.get(f'https://api.example.com/users/{user_id}') as response:
data = await response.json()
print(f"User {user_id}: {data['name']}")
return data
async def main():
"""Run multiple requests concurrently"""
async with aiohttp.ClientSession() as session:
# Both requests start immediately (concurrent)
task1 = get_user(session, 123)
task2 = get_user(session, 456)
# Wait for both to complete
await asyncio.gather(task1, task2)
# Total time ā max(time1, time2) (concurrent, not sequential)
# Run async code
asyncio.run(main())- Non-blocking - better resource utilization
- Handle multiple operations concurrently
- Better performance for I/O-bound operations
- Responsive UI - doesn't freeze
- Efficient for long-running operations
- More complex to implement and understand
- Harder to debug (execution order unclear)
- Callback hell / Promise chaining complexity
- Error handling more complicated
- Race conditions possible
Comprehensive Comparison
| Feature | REST | RPC | Synchronous | Asynchronous |
|---|---|---|---|---|
| Focus | Resources (nouns) | Actions (verbs) | Request-Response | Event-driven |
| Methods | GET, POST, PUT, DELETE | POST (usually) | Any HTTP method | Any HTTP method |
| URL Pattern | /users/123 | /getUser or /rpc | N/A | N/A |
| Execution | Stateless | Can be stateful | Blocking | Non-blocking |
| Caching | ā Easy | ā Difficult | ā Possible | ā ļø Complex |
| Learning Curve | ā ļø Moderate | ā Easy | ā Easy | ā Steep |
| Performance | ā ļø Good | ā Excellent | ā ļø Slower | ā Faster |
| Use Case | Web APIs, CRUD operations | Microservices, actions | Simple requests | Multiple concurrent operations |
| Example | GitHub API, Twitter API | Slack API, Discord API | Form submission | Real-time chat, notifications |
- REST is resource-focused, perfect for CRUD operations on entities
- RPC is action-focused, better for command-based operations
- Synchronous is simple but blocks execution
- Asynchronous is complex but enables concurrent operations
- You can use REST with async or RPC with sync - they're independent concepts
š 2.9 Construct Python Scripts that Call REST APIs
The Python Requests Library
The requests library is the de facto standard for making HTTP requests in Python. It provides a simple, elegant interface for interacting with REST APIs.
# Install using pip pip install requests # Or using pip3 pip3 install requests # Verify installation python -c "import requests; print(requests.__version__)"
Basic Request Structure
#!/usr/bin/env python3
"""
Basic structure of a REST API call using requests library
"""
import requests
# Make a request
response = requests.get('https://api.example.com/users')
# Access response components
status_code = response.status_code # HTTP status code (200, 404, etc.)
headers = response.headers # Response headers (dict-like)
content = response.content # Response body as bytes
text = response.text # Response body as string
data = response.json() # Parse JSON response to dict
ok = response.ok # True if status < 400
# Check if request was successful
if response.ok:
print("Success!")
print(data)
else:
print(f"Error: {response.status_code}")1. GET Requests - Retrieve Data
GET - Reading Resources
GET requests retrieve data from the server without modifying anything.
#!/usr/bin/env python3
"""
GET Request Examples - Retrieving data from REST API
"""
import requests
import json
# =============================================================================
# EXAMPLE 1: Simple GET Request
# =============================================================================
def get_all_users():
"""Get list of all users"""
url = "https://jsonplaceholder.typicode.com/users"
response = requests.get(url)
if response.status_code == 200:
users = response.json()
print(f"ā
Retrieved {len(users)} users")
for user in users[:3]: # Show first 3
print(f" - {user['name']} ({user['email']})")
return users
else:
print(f"ā Error: {response.status_code}")
return None
# =============================================================================
# EXAMPLE 2: GET with Path Parameter (Specific Resource)
# =============================================================================
def get_user_by_id(user_id):
"""Get specific user by ID"""
url = f"https://jsonplaceholder.typicode.com/users/{user_id}"
response = requests.get(url)
if response.status_code == 200:
user = response.json()
print(f"ā
User found:")
print(f" Name: {user['name']}")
print(f" Email: {user['email']}")
print(f" City: {user['address']['city']}")
return user
elif response.status_code == 404:
print(f"ā User {user_id} not found")
return None
else:
print(f"ā Error: {response.status_code}")
return None
# =============================================================================
# EXAMPLE 3: GET with Query Parameters (Filtering)
# =============================================================================
def get_posts_by_user(user_id):
"""Get all posts by specific user"""
url = "https://jsonplaceholder.typicode.com/posts"
# Query parameters as dictionary
params = {
'userId': user_id
}
response = requests.get(url, params=params)
if response.status_code == 200:
posts = response.json()
print(f"ā
User {user_id} has {len(posts)} posts")
for post in posts[:2]: # Show first 2
print(f" - {post['title']}")
return posts
else:
print(f"ā Error: {response.status_code}")
return None
# =============================================================================
# EXAMPLE 4: GET with Multiple Query Parameters
# =============================================================================
def search_posts(user_id=None, limit=None, title_contains=None):
"""Search posts with multiple filters"""
url = "https://jsonplaceholder.typicode.com/posts"
# Build params dynamically
params = {}
if user_id:
params['userId'] = user_id
if limit:
params['_limit'] = limit
response = requests.get(url, params=params)
if response.status_code == 200:
posts = response.json()
# Client-side filtering if needed
if title_contains:
posts = [p for p in posts if title_contains.lower() in p['title'].lower()]
print(f"ā
Found {len(posts)} posts")
return posts
else:
print(f"ā Error: {response.status_code}")
return None
# =============================================================================
# EXAMPLE 5: GET with Headers (Authentication)
# =============================================================================
def get_protected_resource(api_key):
"""Get resource that requires authentication"""
url = "https://api.example.com/protected/data"
headers = {
'Authorization': f'Bearer {api_key}',
'Accept': 'application/json',
'User-Agent': 'MyApp/1.0'
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
print("ā
Access granted")
return response.json()
elif response.status_code == 401:
print("ā Unauthorized - check API key")
return None
else:
print(f"ā Error: {response.status_code}")
return None
# =============================================================================
# RUN EXAMPLES
# =============================================================================
if __name__ == "__main__":
print("="*70)
print("GET REQUEST EXAMPLES")
print("="*70)
print("\n1. Get all users:")
get_all_users()
print("\n2. Get specific user:")
get_user_by_id(1)
print("\n3. Get posts by user:")
get_posts_by_user(1)
print("\n4. Search posts:")
search_posts(user_id=1, limit=3)
2. POST Requests - Create Data
POST - Creating Resources
POST requests send data to the server to create new resources.
#!/usr/bin/env python3
"""
POST Request Examples - Creating new resources
"""
import requests
import json
# =============================================================================
# EXAMPLE 1: Simple POST with JSON Data
# =============================================================================
def create_post(title, body, user_id):
"""Create a new post"""
url = "https://jsonplaceholder.typicode.com/posts"
# Data to send (Python dict, will be converted to JSON)
data = {
"title": title,
"body": body,
"userId": user_id
}
# Headers
headers = {
"Content-Type": "application/json"
}
# Make POST request (json= automatically converts dict to JSON)
response = requests.post(url, json=data, headers=headers)
if response.status_code == 201: # 201 Created
new_post = response.json()
print(f"ā
Post created successfully!")
print(f" Post ID: {new_post['id']}")
print(f" Title: {new_post['title']}")
return new_post
else:
print(f"ā Error: {response.status_code}")
print(f" Message: {response.text}")
return None
# =============================================================================
# EXAMPLE 2: POST with Authentication
# =============================================================================
def create_user(name, email, api_key):
"""Create new user with authentication"""
url = "https://api.example.com/users"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
data = {
"name": name,
"email": email,
"role": "user"
}
response = requests.post(url, json=data, headers=headers)
if response.status_code == 201:
print(f"ā
User created: {response.json()}")
return response.json()
elif response.status_code == 401:
print("ā Unauthorized - invalid API key")
return None
elif response.status_code == 400:
print(f"ā Bad request: {response.json().get('error', 'Unknown error')}")
return None
else:
print(f"ā Error: {response.status_code}")
return None
# =============================================================================
# EXAMPLE 3: POST with Form Data (not JSON)
# =============================================================================
def upload_form_data():
"""Submit form data (application/x-www-form-urlencoded)"""
url = "https://httpbin.org/post"
# Form data as dictionary (use data= not json=)
form_data = {
"username": "johndoe",
"password": "secret123",
"remember": "true"
}
response = requests.post(url, data=form_data)
if response.ok:
print("ā
Form submitted successfully")
return response.json()
else:
print(f"ā Error: {response.status_code}")
return None
# =============================================================================
# EXAMPLE 4: POST with File Upload
# =============================================================================
def upload_file(file_path):
"""Upload file to server"""
url = "https://httpbin.org/post"
# Open and upload file
with open(file_path, 'rb') as f:
files = {'file': f}
data = {'description': 'My file upload'}
response = requests.post(url, files=files, data=data)
if response.ok:
print("ā
File uploaded successfully")
return response.json()
else:
print(f"ā Error: {response.status_code}")
return None
# =============================================================================
# RUN EXAMPLES
# =============================================================================
if __name__ == "__main__":
print("="*70)
print("POST REQUEST EXAMPLES")
print("="*70)
print("\n1. Create new post:")
create_post(
title="My Test Post",
body="This is the content of my post",
user_id=1
)
print("\n2. Submit form data:")
upload_form_data()
3. PUT & PATCH Requests - Update Data
PUT & PATCH - Updating Resources
PUT replaces entire resource. PATCH updates specific fields.
#!/usr/bin/env python3
"""
PUT and PATCH Request Examples - Updating resources
"""
import requests
# =============================================================================
# EXAMPLE 1: PUT - Full Update (Replace Entire Resource)
# =============================================================================
def update_post_full(post_id, title, body, user_id):
"""Update entire post (PUT replaces all fields)"""
url = f"https://jsonplaceholder.typicode.com/posts/{post_id}"
# ALL fields must be provided
data = {
"id": post_id,
"title": title,
"body": body,
"userId": user_id
}
headers = {"Content-Type": "application/json"}
response = requests.put(url, json=data, headers=headers)
if response.status_code == 200:
updated = response.json()
print(f"ā
Post {post_id} updated (PUT)")
print(f" Title: {updated['title']}")
return updated
else:
print(f"ā Error: {response.status_code}")
return None
# =============================================================================
# EXAMPLE 2: PATCH - Partial Update (Update Specific Fields)
# =============================================================================
def update_post_partial(post_id, **fields):
"""Update specific fields only (PATCH for partial update)"""
url = f"https://jsonplaceholder.typicode.com/posts/{post_id}"
# Only send fields to update
data = fields
headers = {"Content-Type": "application/json"}
response = requests.patch(url, json=data, headers=headers)
if response.status_code == 200:
updated = response.json()
print(f"ā
Post {post_id} updated (PATCH)")
print(f" Updated fields: {list(fields.keys())}")
return updated
else:
print(f"ā Error: {response.status_code}")
return None
# =============================================================================
# RUN EXAMPLES
# =============================================================================
if __name__ == "__main__":
print("="*70)
print("PUT & PATCH REQUEST EXAMPLES")
print("="*70)
print("\n1. PUT - Full update:")
update_post_full(
post_id=1,
title="Updated Title",
body="Updated body content",
user_id=1
)
print("\n2. PATCH - Partial update (title only):")
update_post_partial(1, title="Only Title Changed")
print("\n3. PATCH - Partial update (multiple fields):")
update_post_partial(1, title="New Title", body="New Body")
4. DELETE Requests - Remove Data
DELETE - Removing Resources
DELETE requests remove resources from the server.
#!/usr/bin/env python3
"""
DELETE Request Examples - Removing resources
"""
import requests
# =============================================================================
# EXAMPLE 1: Simple DELETE
# =============================================================================
def delete_post(post_id):
"""Delete a post by ID"""
url = f"https://jsonplaceholder.typicode.com/posts/{post_id}"
response = requests.delete(url)
if response.status_code == 200:
print(f"ā
Post {post_id} deleted successfully")
return True
elif response.status_code == 204: # No Content - also success
print(f"ā
Post {post_id} deleted (no content returned)")
return True
elif response.status_code == 404:
print(f"ā Post {post_id} not found")
return False
else:
print(f"ā Error: {response.status_code}")
return False
# =============================================================================
# EXAMPLE 2: DELETE with Authentication
# =============================================================================
def delete_user(user_id, api_key):
"""Delete user (requires authentication)"""
url = f"https://api.example.com/users/{user_id}"
headers = {
"Authorization": f"Bearer {api_key}"
}
response = requests.delete(url, headers=headers)
if response.status_code in [200, 204]:
print(f"ā
User {user_id} deleted")
return True
elif response.status_code == 401:
print("ā Unauthorized")
return False
elif response.status_code == 403:
print("ā Forbidden - insufficient permissions")
return False
elif response.status_code == 404:
print(f"ā User {user_id} not found")
return False
else:
print(f"ā Error: {response.status_code}")
return False
# =============================================================================
# RUN EXAMPLES
# =============================================================================
if __name__ == "__main__":
print("="*70)
print("DELETE REQUEST EXAMPLES")
print("="*70)
print("\n1. Delete post:")
delete_post(1)
5. Complete REST API Client Example
#!/usr/bin/env python3
"""
Complete REST API Client
Demonstrates all CRUD operations with error handling, retries, and best practices
"""
import requests
import time
from typing import Optional, Dict, List
class RESTAPIClient:
"""Generic REST API client with full CRUD operations"""
def __init__(self, base_url: str, api_key: Optional[str] = None):
"""
Initialize API client
Args:
base_url: Base URL for API (e.g., https://api.example.com/v1)
api_key: Optional API key for authentication
"""
self.base_url = base_url.rstrip('/')
self.api_key = api_key
self.session = requests.Session()
# Set default headers
self.session.headers.update({
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'Python-REST-Client/1.0'
})
# Add authentication if provided
if api_key:
self.session.headers.update({
'Authorization': f'Bearer {api_key}'
})
def _make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
"""
Make HTTP request with error handling and retries
Args:
method: HTTP method (GET, POST, PUT, PATCH, DELETE)
endpoint: API endpoint (e.g., '/users/123')
**kwargs: Additional arguments for requests
Returns:
Response object
"""
url = f"{self.base_url}{endpoint}"
max_retries = 3
retry_delay = 1
for attempt in range(max_retries):
try:
response = self.session.request(method, url, **kwargs)
# If rate limited, wait and retry
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', retry_delay))
print(f"ā±ļø Rate limited. Waiting {retry_after}s...")
time.sleep(retry_after)
continue
# If server error, retry
if response.status_code >= 500 and attempt < max_retries - 1:
print(f"ā ļø Server error. Retry {attempt + 1}/{max_retries}...")
time.sleep(retry_delay * (2 ** attempt)) # Exponential backoff
continue
return response
except requests.exceptions.ConnectionError:
if attempt < max_retries - 1:
print(f"ā ļø Connection error. Retry {attempt + 1}/{max_retries}...")
time.sleep(retry_delay * (2 ** attempt))
else:
raise
return response
def get(self, endpoint: str, params: Optional[Dict] = None) -> Optional[Dict]:
"""GET request - Retrieve resource(s)"""
response = self._make_request('GET', endpoint, params=params)
if response.ok:
return response.json() if response.content else None
else:
print(f"ā GET failed: {response.status_code} - {response.text}")
return None
def post(self, endpoint: str, data: Dict) -> Optional[Dict]:
"""POST request - Create new resource"""
response = self._make_request('POST', endpoint, json=data)
if response.status_code == 201:
print("ā
Resource created")
return response.json() if response.content else None
elif response.ok:
return response.json() if response.content else None
else:
print(f"ā POST failed: {response.status_code} - {response.text}")
return None
def put(self, endpoint: str, data: Dict) -> Optional[Dict]:
"""PUT request - Update entire resource"""
response = self._make_request('PUT', endpoint, json=data)
if response.ok:
print("ā
Resource updated (PUT)")
return response.json() if response.content else None
else:
print(f"ā PUT failed: {response.status_code} - {response.text}")
return None
def patch(self, endpoint: str, data: Dict) -> Optional[Dict]:
"""PATCH request - Partially update resource"""
response = self._make_request('PATCH', endpoint, json=data)
if response.ok:
print("ā
Resource updated (PATCH)")
return response.json() if response.content else None
else:
print(f"ā PATCH failed: {response.status_code} - {response.text}")
return None
def delete(self, endpoint: str) -> bool:
"""DELETE request - Remove resource"""
response = self._make_request('DELETE', endpoint)
if response.ok or response.status_code == 204:
print("ā
Resource deleted")
return True
else:
print(f"ā DELETE failed: {response.status_code} - {response.text}")
return False
# =============================================================================
# USAGE EXAMPLE
# =============================================================================
if __name__ == "__main__":
# Initialize client
client = RESTAPIClient('https://jsonplaceholder.typicode.com')
print("="*70)
print("COMPLETE REST API CLIENT DEMONSTRATION")
print("="*70)
# CREATE (POST)
print("\n1ļøā£ CREATE - POST new post:")
new_post = client.post('/posts', {
'title': 'My New Post',
'body': 'This is the content',
'userId': 1
})
if new_post:
print(f" Created post ID: {new_post.get('id')}")
# READ (GET)
print("\n2ļøā£ READ - GET all posts:")
posts = client.get('/posts', params={'_limit': 3})
if posts:
print(f" Retrieved {len(posts)} posts")
print("\n3ļøā£ READ - GET specific post:")
post = client.get('/posts/1')
if post:
print(f" Title: {post.get('title')}")
# UPDATE (PUT)
print("\n4ļøā£ UPDATE - PUT (full update):")
updated = client.put('/posts/1', {
'id': 1,
'title': 'Updated Title',
'body': 'Updated body',
'userId': 1
})
# UPDATE (PATCH)
print("\n5ļøā£ UPDATE - PATCH (partial update):")
patched = client.patch('/posts/1', {
'title': 'Partially Updated Title'
})
# DELETE
print("\n6ļøā£ DELETE - Remove post:")
deleted = client.delete('/posts/1')
print("\n" + "="*70)
print("ā
All CRUD operations completed!")
print("="*70)
- Session Reuse: Use
requests.Session()for connection pooling - Error Handling: Check status codes and handle errors gracefully
- Retry Logic: Implement exponential backoff for transient failures
- Rate Limiting: Respect
Retry-Afterheaders - Authentication: Centralize header management
- Type Hints: Use type annotations for clarity
š Interactive Python Interpreter
- Write Python code in the editor below
- Click "ā¶ Run Code" to execute
- Output appears in the console below
- Use sample codes for quick testing
- Experiment with REST API patterns and styles