Before we look at the code, note the specific professional features we are implementing:
- The Pattern: Strict adherence to Make → Check → Extract
- Validation: Checks
response.ok and Content-Type headers
- Safety: Wraps network calls in robust
try/except blocks
- Binary Handling: Uses
response.content and saves with "wb" mode
- Predictable Returns: Returns a tuple instead of raising unhandled exceptions
Pay close attention to the return statement. The function returns a tuple containing:
(success_boolean, status_message, filename).
This design allows the main program to easily check if the download worked without needing its own complex error handling logic.
Complete Implementation
"""
Professional Image Downloader
Demonstrates all Chapter 4 concepts in one practical application.
"""
import requests
def download_image(url, filename=None):
"""
Download an image with professional error handling and validation.
Args:
url: URL of the image to download
filename: Optional filename; if None, will be auto-generated
Returns:
Tuple of (success: bool, message: str, filename: str or None)
"""
try:
# STEP 1: MAKE the request
print(f"Downloading from {url}...")
response = requests.get(url, timeout=10)
# STEP 2: CHECK the response
# Check status code
if not response.ok:
return (
False,
f"Request failed: {response.status_code} {response.reason}",
None
)
# Verify content type using headers
content_type = response.headers.get("Content-Type", "")
if not content_type.startswith("image/"):
return (
False,
f"Expected image but received {content_type}",
None
)
# STEP 3: EXTRACT and save the data
# Determine file extension from Content-Type
if filename is None:
extension_map = {
"image/png": "png",
"image/jpeg": "jpg",
"image/gif": "gif",
"image/webp": "webp",
}
extension = extension_map.get(content_type, "bin")
filename = f"downloaded_image.{extension}"
# Save binary data
try:
with open(filename, "wb") as f:
f.write(response.content)
except IOError as e:
return (False, f"Could not save file: {e}", None)
# Calculate file size for feedback
size_kb = len(response.content) / 1024
return (
True,
f"Successfully downloaded {size_kb:.1f} KB",
filename
)
except requests.exceptions.Timeout:
return (False, "Request timed out", None)
except requests.exceptions.ConnectionError:
return (False, "Could not connect to server", None)
except requests.exceptions.RequestException as e:
return (False, f"Request failed: {e}", None)
def main():
"""Test the downloader with various scenarios."""
print("IMAGE DOWNLOADER TEST SUITE")
print("=" * 60)
test_cases = [
{
"url": "https://httpbin.org/image/png",
"description": "Valid PNG image",
"filename": "test_image.png"
},
{
"url": "https://httpbin.org/image/jpeg",
"description": "Valid JPEG image",
"filename": "test_image.jpg"
},
{
"url": "https://httpbin.org/status/404",
"description": "404 Not Found error",
"filename": "should_fail.png"
},
{
"url": "https://httpbin.org/json",
"description": "JSON instead of image",
"filename": "should_fail.png"
},
]
for i, test in enumerate(test_cases, 1):
print(f"\nTest {i}: {test['description']}")
print("-" * 60)
success, message, saved_filename = download_image(
test["url"],
test.get("filename")
)
if success:
print(f"✅ SUCCESS: {message}")
print(f" Saved as: {saved_filename}")
else:
print(f"❌ FAILED: {message}")
print("\n" + "=" * 60)
print("Test suite complete. Check your project folder for downloaded images.")
if __name__ == "__main__":
main()
IMAGE DOWNLOADER TEST SUITE
============================================================
Test 1: Valid PNG image
------------------------------------------------------------
Downloading from https://httpbin.org/image/png...
✅ SUCCESS: Successfully downloaded 8.3 KB
Saved as: test_image.png
Test 2: Valid JPEG image
------------------------------------------------------------
Downloading from https://httpbin.org/image/jpeg...
✅ SUCCESS: Successfully downloaded 35.4 KB
Saved as: test_image.jpg
Test 3: 404 Not Found error
------------------------------------------------------------
Downloading from https://httpbin.org/status/404...
❌ FAILED: Request failed: 404 NOT FOUND
Test 4: JSON instead of image
------------------------------------------------------------
Downloading from https://httpbin.org/json...
❌ FAILED: Expected image but received application/json
============================================================
Test suite complete. Check your project folder for downloaded images.
Pro Tip: Real-World Content Types
Our code uses .startswith("image/") for a specific reason: headers can be complex. A server might return image/png; charset=utf-8, which would fail an exact match check like == "image/png".
Note: Some storage APIs (like AWS S3) may return the generic application/octet-stream even for images. In those edge cases, you can't rely on headers alone and might need to trust the URL extension or inspect the file's binary "magic bytes" after downloading.
Techniques Applied
This downloader demonstrates every major concept from Chapter 4:
- Make → Check → Extract pattern: Three clear steps with validation at each stage
- Status code validation: Checks
response.ok before processing
- Header analysis: Uses
Content-Type to verify image and determine extension
- Binary data handling: Uses
response.content and "wb" mode
- Comprehensive error handling: try/except for network, status, content type, and file I/O errors
- User-friendly feedback: Returns structured results with clear messages