Practical API Example: Building an API Client Class in Python In professional software development, API calls should not be scattered haphazardly throughout codebase files. Instead, best practices dictate encapsulating HTTP interactions inside a dedicated API Client Class that manages base URLs, authentication tokens, connection pooling via requests.Session, pagination, and error handling.
When you call requests.get() repeatedly, Python opens and closes a new TCP connection for every single request. Using requests.Session() reuses the underlying TCP connection (via HTTP keep-alive), resulting in significantly faster requests and shared headers/cookies.
1 import requests
2
3 # Reusable session object with persistent headers
4 session = requests . Session ( )
5 session . headers . update ( {
6 "User-Agent" : "DevClient/1.0" ,
7 "Accept" : "application/json"
8 } )
9
10 # Both requests share the same TCP connection pool:
11 r1 = session . get ( "https://jsonplaceholder.typicode.com/users/1" )
12 r2 = session . get ( "https://jsonplaceholder.typicode.com/users/2" )
Let's build a clean, production-ready client for querying public GitHub repositories and user profiles:
Visual Architecture & Process Flow How data and code flow step-by-step
Flowchart def _request
self, endpoint: str, params: Optional[Dict[str, Any]] = None
Notice how clean and intuitive the caller code becomes when the HTTP details are cleanly encapsulated:
1 def main ( ) :
2 client = GitHubClient ( )
3
4 # 1. Fetch user profile
5 username = "octocat"
6 profile = client . get_user_profile ( username )
7
8 if profile :
9 print ( "=" * 45 )
10 print ( f"Profile: { profile . get ( 'name' ) } (@ { profile . get ( 'login' ) } )" )
11 print ( f"Bio: { profile . get ( 'bio' ) } " )
12 print ( f"Public Repos: { profile . get ( 'public_repos' ) } " )
13 print ( f"Followers: { profile . get ( 'followers' ) } " )
14 print ( "=" * 45 )
15
16 # 2. Fetch top repositories
17 repos = client . get_user_repositories ( username , limit = 3 )
18 print ( f"\nTop Repositories for @ { username } :" )
19 for r in repos :
20 print ( f" - { r [ 'name' ] } : ⭐ { r [ 'stargazers_count' ] } | Language: { r . get ( 'language' ) or 'N/A' } " )
21
22 if __name__ == "__main__" :
23 main ( )
1 Keep Base URLs Configurable : Define BASE_URL as a class or instance variable to facilitate swapping staging/production environments.
2 Centralize Error Handling : Use an internal _request() method so retry logic, rate limit checking, and timeouts are handled in one place.
3 Inspect Rate-Limit Headers : Many APIs return X-RateLimit-Remaining and X-RateLimit-Reset headers. Monitoring these prevents unexpected 403 Forbidden errors.
4 Use Session Objects : requests.Session handles HTTP connection pooling and avoids recreating TCP/TLS handshakes on every call.
1. Hardcoding API Keys in Public Code Never commit private API keys to Git repositories. Always load secrets from environment variables using os.getenv("API_KEY").
2. Ignoring Rate Limits (HTTP 429) Public APIs enforce rate limits. If your script loops too fast, you will be blocked. Add gentle pauses with time.sleep() or check rate limit headers.
Scenario Fetch an inspirational quote from the public Quotable API (https://dummyjson.com/quotes/random), cleanly extracting the quote and author.
Complete Solution 1 import requests
2
3 def fetch_random_quote ( ) :
4 url = "https://dummyjson.com/quotes/random"
5 try :
6 res = requests . get ( url , timeout = 5.0 )
7 res . raise_for_status ( )
8 quote_data = res . json ( )
9 print ( f"\" { quote_data . get ( 'quote' ) } \"" )
10 print ( f" — { quote_data . get ( 'author' ) } " )
11 except requests . exceptions . RequestException as e :
12 print ( f"Could not retrieve quote: { e } " )
13
14 fetch_random_quote ( )
Expected Output 1 "Life isn't about finding yourself. Life is about creating yourself."
2 — George Bernard Shaw
Interactive Quiz • Knowledge Check
Practice Quiz What is the primary performance benefit of using requests.Session() over calling requests.get() repeatedly?
A It compiles Python scripts into C binaries
B It reuses underlying TCP connections (connection pooling) across multiple requests
C It bypasses internet firewalls
D It compresses all response strings into gzip automatically
What HTTP status code typically indicates that an API client has exceeded its rate limit quota?
A 200 OK
B 404 Not Found
C 429 Too Many Requests (or 403 Forbidden with rate headers)
D 500 Internal Server Error
Why is it advantageous to route all API calls through an internal _request() helper method?
A It eliminates the need for unit testing
B Centralizing request execution allows uniform error handling, header injection, logging, and timeouts
C Python requires helper methods for all network operations
D It makes the class immutable
Which header returned by GitHub and many modern APIs informs you how many requests remain in your quota?
A X-Cache-Status
B X-RateLimit-Remaining
C ETag
D Content-Encoding
In our GitHubClient, how are query parameters passed to limit the number of repositories returned?
A By appending #limit=5 to the URL
B By passing params={"per_page": limit} to the session GET request
C By modifying the HTTP Host header
D By setting an environment variable
In the next lesson, we will continue your intermediate Python journey with Project: Weather Data Fetcher (8: Working with APIs).
👉 Continue to Next Lesson: Project: Weather Data Fetcher →