The Beginner's Guide to APIs in Python: From Basics to Real Projects
APIs (Application Programming Interfaces) are the backbone of modern web development, enabling different software applications to communicate with each other. Whether you're fetching weather data, posting to social media, or integrating payment systems, APIs make it all possible. This comprehensive guide will take you from API novice to confident practitioner, covering everything from basic concepts to building real-world projects.
What is an API?
An API acts as a messenger between different software applications. Think of it as a waiter in a restaurant: you (the client) tell the waiter (API) what you want from the menu, the waiter takes your order to the kitchen (server), and then brings back your food (data). APIs define the methods and data formats that applications can use to communicate with each other.
Types of APIs
REST APIs are the most common type, using standard HTTP methods (GET, POST, PUT, DELETE) to interact with resources. They're stateless, meaning each request contains all the information needed to process it.
GraphQL APIs allow clients to request specific data fields, reducing over-fetching of information.
SOAP APIs use XML for message formatting and are more rigid but offer built-in error handling.
For this guide, we'll focus primarily on REST APIs since they're the most widely used and beginner-friendly.
Setting Up Your Python Environment
Before diving into API calls, let's set up the necessary tools:
`python
Install required packages
pip install requests pip install python-dotenv # For environment variables pip install beautifulsoup4 # For HTML parsing (optional)`The requests library is Python's go-to tool for making HTTP requests. It's simple, elegant, and powerful.
Making Your First API Call
Let's start with a simple example using a free API that doesn't require authentication:
`python
import requests
import json
Make a GET request to a public API
response = requests.get('https://jsonplaceholder.typicode.com/posts/1')Check if the request was successful
if response.status_code == 200: # Parse JSON response data = response.json() print(f"Title: {data['title']}") print(f"Body: {data['body']}") else: print(f"Error: {response.status_code}")`This code demonstrates the basic structure of an API call:
1. Make the request using requests.get()
2. Check the status code (200 means success)
3. Parse the JSON response
4. Use the data
Understanding HTTP Status Codes
- 200: Success - 201: Created (successful POST request) - 400: Bad Request (client error) - 401: Unauthorized (authentication required) - 404: Not Found - 500: Internal Server Error
Handling JSON Data
JSON (JavaScript Object Notation) is the standard format for API data exchange. Python's requests library makes JSON handling incredibly easy:
`python
import requests
def fetch_user_data(user_id): """Fetch user data from JSONPlaceholder API""" url = f'https://jsonplaceholder.typicode.com/users/{user_id}' try: response = requests.get(url) response.raise_for_status() # Raises an exception for bad status codes user_data = response.json() # Extract specific information user_info = { 'name': user_data['name'], 'email': user_data['email'], 'phone': user_data['phone'], 'website': user_data['website'], 'company': user_data['company']['name'] } return user_info except requests.exceptions.RequestException as e: print(f"Error fetching user data: {e}") return None
Usage
user = fetch_user_data(1) if user: for key, value in user.items(): print(f"{key.capitalize()}: {value}")`Working with Complex JSON Structures
Real-world APIs often return nested JSON data. Here's how to navigate complex structures:
`python
def parse_complex_json():
"""Example of handling nested JSON data"""
response = requests.get('https://jsonplaceholder.typicode.com/users')
users = response.json()
for user in users:
print(f"User: {user['name']}")
print(f"Address: {user['address']['street']}, {user['address']['city']}")
print(f"Coordinates: {user['address']['geo']['lat']}, {user['address']['geo']['lng']}")
print("-" * 40)
parse_complex_json()
`
HTTP Methods: GET, POST, PUT, DELETE
Different HTTP methods serve different purposes:
GET Requests
Used to retrieve data:`python
def get_posts():
"""Retrieve all posts"""
response = requests.get('https://jsonplaceholder.typicode.com/posts')
return response.json()
def get_post_by_id(post_id):
"""Retrieve a specific post"""
response = requests.get(f'https://jsonplaceholder.typicode.com/posts/{post_id}')
return response.json()
`
POST Requests
Used to create new resources:`python
def create_post(title, body, user_id):
"""Create a new post"""
url = 'https://jsonplaceholder.typicode.com/posts'
post_data = {
'title': title,
'body': body,
'userId': user_id
}
response = requests.post(url, json=post_data)
if response.status_code == 201:
print("Post created successfully!")
return response.json()
else:
print(f"Error creating post: {response.status_code}")
return None
Usage
new_post = create_post("My API Post", "This post was created via API!", 1) print(new_post)`PUT Requests
Used to update existing resources:`python
def update_post(post_id, title, body, user_id):
"""Update an existing post"""
url = f'https://jsonplaceholder.typicode.com/posts/{post_id}'
updated_data = {
'id': post_id,
'title': title,
'body': body,
'userId': user_id
}
response = requests.put(url, json=updated_data)
if response.status_code == 200:
print("Post updated successfully!")
return response.json()
else:
print(f"Error updating post: {response.status_code}")
return None
`
DELETE Requests
Used to remove resources:`python
def delete_post(post_id):
"""Delete a post"""
url = f'https://jsonplaceholder.typicode.com/posts/{post_id}'
response = requests.delete(url)
if response.status_code == 200:
print("Post deleted successfully!")
return True
else:
print(f"Error deleting post: {response.status_code}")
return False
`
Authentication Methods
Most real-world APIs require authentication to ensure security and track usage.
API Keys
API keys are the simplest form of authentication:
`python
import os
from dotenv import load_dotenv
Load environment variables
load_dotenv()def weather_api_example():
"""Example using OpenWeatherMap API with API key"""
api_key = os.getenv('OPENWEATHER_API_KEY') # Store in .env file
city = 'London'
url = f'http://api.openweathermap.org/data/2.5/weather'
params = {
'q': city,
'appid': api_key,
'units': 'metric'
}
response = requests.get(url, params=params)
if response.status_code == 200:
weather_data = response.json()
print(f"Temperature in {city}: {weather_data['main']['temp']}°C")
print(f"Description: {weather_data['weather'][0]['description']}")
else:
print(f"Error: {response.status_code}")
`
Bearer Token Authentication
Many modern APIs use bearer tokens:
`python
def bearer_token_example():
"""Example of using bearer token authentication"""
token = os.getenv('API_BEARER_TOKEN')
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
response = requests.get('https://api.example.com/data', headers=headers)
return response.json()
`
Basic Authentication
Some APIs use username/password authentication:
`python
def basic_auth_example():
"""Example of basic authentication"""
username = os.getenv('API_USERNAME')
password = os.getenv('API_PASSWORD')
response = requests.get(
'https://api.example.com/data',
auth=(username, password)
)
return response.json()
`
OAuth 2.0
OAuth is more complex but provides better security:
`python
import requests
from requests_oauth2 import OAuth2BearerToken
def oauth_example():
"""Basic OAuth 2.0 example"""
# First, get an access token
token_url = 'https://api.example.com/oauth/token'
token_data = {
'grant_type': 'client_credentials',
'client_id': os.getenv('CLIENT_ID'),
'client_secret': os.getenv('CLIENT_SECRET')
}
token_response = requests.post(token_url, data=token_data)
access_token = token_response.json()['access_token']
# Use the token for API calls
headers = {'Authorization': f'Bearer {access_token}'}
response = requests.get('https://api.example.com/data', headers=headers)
return response.json()
`
Error Handling and Best Practices
Robust error handling is crucial for production applications:
`python
import time
from requests.exceptions import RequestException, Timeout, ConnectionError
class APIClient:
def __init__(self, base_url, api_key=None, timeout=30):
self.base_url = base_url
self.api_key = api_key
self.timeout = timeout
self.session = requests.Session()
if api_key:
self.session.headers.update({'Authorization': f'Bearer {api_key}'})
def make_request(self, method, endpoint, kwargs):
"""Make API request with error handling and retries"""
url = f"{self.base_url}/{endpoint.lstrip('/')}"
for attempt in range(3): # Retry up to 3 times
try:
response = self.session.request(
method,
url,
timeout=self.timeout,
kwargs
)
response.raise_for_status()
return response.json()
except Timeout:
print(f"Timeout on attempt {attempt + 1}")
if attempt == 2:
raise
time.sleep(2 attempt) # Exponential backoff
except ConnectionError:
print(f"Connection error on attempt {attempt + 1}")
if attempt == 2:
raise
time.sleep(2 attempt)
except requests.exceptions.HTTPError as e:
if response.status_code == 429: # Rate limit
print("Rate limit exceeded, waiting...")
time.sleep(60)
continue
else:
print(f"HTTP error: {e}")
raise
except RequestException as e:
print(f"Request error: {e}")
raise
def get(self, endpoint, kwargs):
return self.make_request('GET', endpoint, kwargs)
def post(self, endpoint, kwargs):
return self.make_request('POST', endpoint, kwargs)
def put(self, endpoint, kwargs):
return self.make_request('PUT', endpoint, kwargs)
def delete(self, endpoint, kwargs):
return self.make_request('DELETE', endpoint, kwargs)
`
Project 1: Weather Dashboard
Let's build a weather dashboard that fetches current weather and forecasts:
`python
import requests
import json
from datetime import datetime
import os
class WeatherDashboard: def __init__(self, api_key): self.api_key = api_key self.base_url = "http://api.openweathermap.org/data/2.5" def get_current_weather(self, city): """Get current weather for a city""" endpoint = f"{self.base_url}/weather" params = { 'q': city, 'appid': self.api_key, 'units': 'metric' } try: response = requests.get(endpoint, params=params) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Error fetching weather data: {e}") return None def get_forecast(self, city, days=5): """Get weather forecast for a city""" endpoint = f"{self.base_url}/forecast" params = { 'q': city, 'appid': self.api_key, 'units': 'metric', 'cnt': days * 8 # 8 forecasts per day (every 3 hours) } try: response = requests.get(endpoint, params=params) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Error fetching forecast data: {e}") return None def display_current_weather(self, weather_data): """Display current weather information""" if not weather_data: print("No weather data available") return city = weather_data['name'] country = weather_data['sys']['country'] temp = weather_data['main']['temp'] feels_like = weather_data['main']['feels_like'] humidity = weather_data['main']['humidity'] description = weather_data['weather'][0]['description'] print(f"\n🌤️ Current Weather in {city}, {country}") print("=" * 40) print(f"Temperature: {temp}°C (feels like {feels_like}°C)") print(f"Condition: {description.title()}") print(f"Humidity: {humidity}%") def display_forecast(self, forecast_data): """Display weather forecast""" if not forecast_data: print("No forecast data available") return print(f"\n📅 5-Day Forecast") print("=" * 40) # Group forecasts by date daily_forecasts = {} for item in forecast_data['list']: date = datetime.fromtimestamp(item['dt']).strftime('%Y-%m-%d') if date not in daily_forecasts: daily_forecasts[date] = [] daily_forecasts[date].append(item) # Display daily averages for date, forecasts in list(daily_forecasts.items())[:5]: temps = [f['main']['temp'] for f in forecasts] avg_temp = sum(temps) / len(temps) condition = forecasts[0]['weather'][0]['description'] print(f"{date}: {avg_temp:.1f}°C - {condition.title()}")
def main(): # You'll need to get an API key from OpenWeatherMap api_key = os.getenv('OPENWEATHER_API_KEY') if not api_key: print("Please set your OpenWeatherMap API key in environment variables") return dashboard = WeatherDashboard(api_key) while True: city = input("\nEnter city name (or 'quit' to exit): ").strip() if city.lower() == 'quit': break if not city: print("Please enter a valid city name") continue # Get and display current weather current_weather = dashboard.get_current_weather(city) dashboard.display_current_weather(current_weather) # Get and display forecast forecast = dashboard.get_forecast(city) dashboard.display_forecast(forecast)
if __name__ == "__main__":
main()
`
Project 2: GitHub Repository Analyzer
This project demonstrates working with the GitHub API to analyze repositories:
`python
import requests
import json
from datetime import datetime, timedelta
from collections import Counter
class GitHubAnalyzer: def __init__(self, token=None): self.base_url = "https://api.github.com" self.session = requests.Session() if token: self.session.headers.update({ 'Authorization': f'token {token}', 'Accept': 'application/vnd.github.v3+json' }) def get_user_info(self, username): """Get user information""" url = f"{self.base_url}/users/{username}" try: response = self.session.get(url) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Error fetching user info: {e}") return None def get_user_repos(self, username, per_page=30): """Get user repositories""" url = f"{self.base_url}/users/{username}/repos" params = { 'per_page': per_page, 'sort': 'updated', 'direction': 'desc' } try: response = self.session.get(url, params=params) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Error fetching repositories: {e}") return None def get_repo_languages(self, username, repo_name): """Get programming languages used in a repository""" url = f"{self.base_url}/repos/{username}/{repo_name}/languages" try: response = self.session.get(url) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Error fetching languages: {e}") return None def analyze_user(self, username): """Comprehensive user analysis""" print(f"🔍 Analyzing GitHub user: {username}") print("=" * 50) # Get user info user_info = self.get_user_info(username) if not user_info: return # Display user information print(f"Name: {user_info.get('name', 'N/A')}") print(f"Bio: {user_info.get('bio', 'N/A')}") print(f"Public Repos: {user_info['public_repos']}") print(f"Followers: {user_info['followers']}") print(f"Following: {user_info['following']}") print(f"Created: {user_info['created_at'][:10]}") # Get repositories repos = self.get_user_repos(username) if not repos: return # Analyze repositories total_stars = sum(repo['stargazers_count'] for repo in repos) total_forks = sum(repo['forks_count'] for repo in repos) # Language analysis all_languages = Counter() for repo in repos: if not repo['fork']: # Skip forked repositories languages = self.get_repo_languages(username, repo['name']) if languages: all_languages.update(languages) print(f"\n📊 Repository Statistics:") print(f"Total Stars: {total_stars}") print(f"Total Forks: {total_forks}") print(f"\n💻 Top Programming Languages:") for lang, bytes_count in all_languages.most_common(5): percentage = (bytes_count / sum(all_languages.values())) * 100 print(f"{lang}: {percentage:.1f}%") print(f"\n⭐ Most Popular Repositories:") sorted_repos = sorted(repos, key=lambda x: x['stargazers_count'], reverse=True) for repo in sorted_repos[:5]: if repo['stargazers_count'] > 0: print(f"{repo['name']}: {repo['stargazers_count']} stars") print(f" Description: {repo['description'] or 'No description'}") print(f" Language: {repo['language'] or 'Unknown'}") print()
def main(): # GitHub token is optional but recommended for higher rate limits token = os.getenv('GITHUB_TOKEN') analyzer = GitHubAnalyzer(token) while True: username = input("\nEnter GitHub username (or 'quit' to exit): ").strip() if username.lower() == 'quit': break if not username: print("Please enter a valid username") continue analyzer.analyze_user(username)
if __name__ == "__main__":
main()
`
Project 3: News Aggregator
This project creates a news aggregator using a news API:
`python
import requests
import json
from datetime import datetime, timedelta
import webbrowser
class NewsAggregator: def __init__(self, api_key): self.api_key = api_key self.base_url = "https://newsapi.org/v2" def get_top_headlines(self, country='us', category=None, page_size=10): """Get top headlines""" endpoint = f"{self.base_url}/top-headlines" params = { 'apiKey': self.api_key, 'country': country, 'pageSize': page_size } if category: params['category'] = category try: response = requests.get(endpoint, params=params) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Error fetching headlines: {e}") return None def search_news(self, query, from_date=None, sort_by='publishedAt', page_size=10): """Search for news articles""" endpoint = f"{self.base_url}/everything" params = { 'apiKey': self.api_key, 'q': query, 'sortBy': sort_by, 'pageSize': page_size, 'language': 'en' } if from_date: params['from'] = from_date try: response = requests.get(endpoint, params=params) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Error searching news: {e}") return None def display_articles(self, news_data, show_content=False): """Display news articles""" if not news_data or 'articles' not in news_data: print("No articles found") return articles = news_data['articles'] print(f"\nFound {len(articles)} articles:") print("=" * 80) for i, article in enumerate(articles, 1): print(f"\n{i}. {article['title']}") print(f"Source: {article['source']['name']}") print(f"Published: {article['publishedAt'][:10]}") if article['description']: print(f"Description: {article['description']}") if show_content and article['content']: content = article['content'][:200] + "..." if len(article['content']) > 200 else article['content'] print(f"Content: {content}") print(f"URL: {article['url']}") print("-" * 40) def save_articles(self, news_data, filename): """Save articles to JSON file""" if not news_data: return False try: with open(filename, 'w', encoding='utf-8') as f: json.dump(news_data, f, indent=2, ensure_ascii=False) print(f"Articles saved to {filename}") return True except Exception as e: print(f"Error saving articles: {e}") return False
def main(): api_key = os.getenv('NEWS_API_KEY') if not api_key: print("Please set your News API key in environment variables") print("Get one free at: https://newsapi.org/") return aggregator = NewsAggregator(api_key) while True: print("\n📰 News Aggregator") print("1. Top Headlines") print("2. Search News") print("3. Category News") print("4. Quit") choice = input("\nEnter your choice (1-4): ").strip() if choice == '1': country = input("Enter country code (us, gb, fr, etc.) or press Enter for US: ").strip() or 'us' news = aggregator.get_top_headlines(country=country) aggregator.display_articles(news) elif choice == '2': query = input("Enter search query: ").strip() if query: days_back = input("How many days back? (default: 7): ").strip() try: days = int(days_back) if days_back else 7 from_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d') except ValueError: from_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d') news = aggregator.search_news(query, from_date=from_date) aggregator.display_articles(news) save = input("\nSave articles to file? (y/n): ").strip().lower() if save == 'y': filename = f"news_{query.replace(' ', '_')}_{datetime.now().strftime('%Y%m%d')}.json" aggregator.save_articles(news, filename) elif choice == '3': categories = ['business', 'entertainment', 'general', 'health', 'science', 'sports', 'technology'] print(f"Available categories: {', '.join(categories)}") category = input("Enter category: ").strip().lower() if category in categories: news = aggregator.get_top_headlines(category=category) aggregator.display_articles(news) else: print("Invalid category") elif choice == '4': print("Thanks for using News Aggregator!") break else: print("Invalid choice. Please try again.")
if __name__ == "__main__":
main()
`
Advanced Topics
Rate Limiting and Throttling
Many APIs have rate limits. Here's how to handle them gracefully:
`python
import time
from functools import wraps
def rate_limit(calls_per_second=1): """Decorator to rate limit function calls""" min_interval = 1.0 / calls_per_second last_called = [0.0] def decorator(func): @wraps(func) def wrapper(args, *kwargs): elapsed = time.time() - last_called[0] left_to_wait = min_interval - elapsed if left_to_wait > 0: time.sleep(left_to_wait) ret = func(args, *kwargs) last_called[0] = time.time() return ret return wrapper return decorator
class RateLimitedAPIClient:
def __init__(self, base_url, rate_limit=10):
self.base_url = base_url
self.session = requests.Session()
self.rate_limit = rate_limit
self.last_request_time = 0
def _enforce_rate_limit(self):
"""Enforce rate limiting between requests"""
current_time = time.time()
time_since_last = current_time - self.last_request_time
min_interval = 1.0 / self.rate_limit
if time_since_last < min_interval:
sleep_time = min_interval - time_since_last
time.sleep(sleep_time)
self.last_request_time = time.time()
def get(self, endpoint, kwargs):
self._enforce_rate_limit()
url = f"{self.base_url}/{endpoint.lstrip('/')}"
return self.session.get(url, kwargs)
`
Pagination
Many APIs return paginated results:
`python
def fetch_all_pages(base_url, params=None, max_pages=None):
"""Fetch all pages from a paginated API"""
all_data = []
page = 1
params = params or {}
while True:
params['page'] = page
response = requests.get(base_url, params=params)
if response.status_code != 200:
break
data = response.json()
# Adjust based on API structure
if 'results' in data:
items = data['results']
elif 'data' in data:
items = data['data']
else:
items = data
if not items:
break
all_data.extend(items)
# Check if we've reached max pages or if there are no more pages
if max_pages and page >= max_pages:
break
if 'next' not in data or not data['next']:
break
page += 1
time.sleep(0.1) # Be nice to the API
return all_data
`
Caching API Responses
Implement caching to reduce API calls and improve performance:
`python
import json
import os
import time
from datetime import datetime, timedelta
class APICache: def __init__(self, cache_dir='api_cache', default_ttl=3600): self.cache_dir = cache_dir self.default_ttl = default_ttl if not os.path.exists(cache_dir): os.makedirs(cache_dir) def _get_cache_path(self, key): """Get cache file path for a given key""" return os.path.join(self.cache_dir, f"{key}.json") def get(self, key): """Get cached data""" cache_path = self._get_cache_path(key) if not os.path.exists(cache_path): return None try: with open(cache_path, 'r') as f: cache_data = json.load(f) # Check if cache is expired cached_time = datetime.fromisoformat(cache_data['cached_at']) if datetime.now() - cached_time > timedelta(seconds=cache_data['ttl']): os.remove(cache_path) return None return cache_data['data'] except (json.JSONDecodeError, KeyError, ValueError): # Invalid cache file, remove it os.remove(cache_path) return None def set(self, key, data, ttl=None): """Cache data""" cache_path = self._get_cache_path(key) ttl = ttl or self.default_ttl cache_data = { 'data': data, 'cached_at': datetime.now().isoformat(), 'ttl': ttl } try: with open(cache_path, 'w') as f: json.dump(cache_data, f, indent=2) except Exception as e: print(f"Error caching data: {e}")
class CachedAPIClient:
def __init__(self, base_url, cache_ttl=3600):
self.base_url = base_url
self.cache = APICache(default_ttl=cache_ttl)
self.session = requests.Session()
def get(self, endpoint, use_cache=True, kwargs):
"""Make GET request with caching"""
cache_key = f"{endpoint}_{hash(str(sorted(kwargs.items())))}"
if use_cache:
cached_data = self.cache.get(cache_key)
if cached_data is not None:
print(f"Using cached data for {endpoint}")
return cached_data
# Make API request
url = f"{self.base_url}/{endpoint.lstrip('/')}"
response = self.session.get(url, kwargs)
response.raise_for_status()
data = response.json()
if use_cache:
self.cache.set(cache_key, data)
return data
`
Testing Your API Code
Testing is crucial for reliable API integrations:
`python
import unittest
from unittest.mock import patch, Mock
import requests
class TestWeatherAPI(unittest.TestCase): def setUp(self): self.api_key = "test_api_key" self.weather_client = WeatherDashboard(self.api_key) @patch('requests.get') def test_get_current_weather_success(self, mock_get): # Mock successful API response mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = { 'name': 'London', 'sys': {'country': 'GB'}, 'main': {'temp': 20, 'feels_like': 18, 'humidity': 65}, 'weather': [{'description': 'partly cloudy'}] } mock_response.raise_for_status.return_value = None mock_get.return_value = mock_response result = self.weather_client.get_current_weather('London') self.assertIsNotNone(result) self.assertEqual(result['name'], 'London') self.assertEqual(result['main']['temp'], 20) @patch('requests.get') def test_get_current_weather_api_error(self, mock_get): # Mock API error mock_get.side_effect = requests.exceptions.RequestException("API Error") result = self.weather_client.get_current_weather('London') self.assertIsNone(result)
if __name__ == '__main__':
unittest.main()
`
Security Best Practices
Environment Variables
Never hardcode API keys in your source code:
`python
.env file
OPENWEATHER_API_KEY=your_api_key_here GITHUB_TOKEN=your_github_token_here NEWS_API_KEY=your_news_api_key_hereIn your Python code
import os from dotenv import load_dotenvload_dotenv()
api_key = os.getenv('OPENWEATHER_API_KEY')
if not api_key:
raise ValueError("API key not found in environment variables")
`
Input Validation
Always validate user inputs:
`python
import re
def validate_city_name(city): """Validate city name input""" if not city or not city.strip(): return False, "City name cannot be empty" if len(city) > 100: return False, "City name too long" # Allow letters, spaces, hyphens, and apostrophes if not re.match(r"^[a-zA-Z\s\-']+$", city): return False, "City name contains invalid characters" return True, ""
def safe_api_call(city):
"""Make API call with input validation"""
is_valid, error_message = validate_city_name(city)
if not is_valid:
print(f"Invalid input: {error_message}")
return None
# Proceed with API call
return get_weather_data(city)
`
Conclusion
APIs are powerful tools that enable modern applications to communicate and share data seamlessly. Throughout this guide, we've covered:
- Basic API concepts and how to make your first API calls - HTTP methods (GET, POST, PUT, DELETE) and when to use each - Authentication methods including API keys, bearer tokens, and OAuth - Error handling and best practices for robust applications - Real-world projects demonstrating practical API usage - Advanced topics like rate limiting, pagination, and caching - Security practices to keep your applications safe
The key to mastering APIs is practice. Start with simple public APIs, then gradually work with more complex services that require authentication. Remember to:
1. Read the documentation thoroughly before starting 2. Handle errors gracefully with proper exception handling 3. Respect rate limits to maintain good API citizenship 4. Keep your credentials secure using environment variables 5. Test your code to ensure reliability 6. Cache responses when appropriate to improve performance
As you continue your API journey, explore different types of APIs, experiment with various authentication methods, and build projects that solve real problems. The skills you've learned here will serve as a solid foundation for integrating any API into your Python applications.
Whether you're building web applications, data analysis tools, or automation scripts, APIs will be an essential part of your toolkit. Keep practicing, stay curious, and don't hesitate to dive into the documentation of new APIs you want to explore.