DEV Community

Valerie  Sims
Valerie Sims

Posted on

Building a Simple Python Script to Bulk Download Images

If you've ever needed to grab a bunch of images from a list of URLs — for a dataset, a backup, a scraping project, or just to save some references — writing your own downloader is one of those small utilities that pays for itself pretty quickly. It's also a nice beginner-friendly project if you're getting comfortable with the requests library and basic file handling in Python.

In this post, I'll walk through a lightweight script I put together that downloads single or multiple images, handles errors gracefully, and organizes everything into a folder automatically.

Why Not Just Right-Click and Save?

For one or two images, sure, manual saving is fine. But once you're dealing with dozens (or hundreds) of URLs, doing it by hand gets tedious fast — and error-prone. A script gives you:

  • Consistency — every image lands in the same folder with a predictable naming pattern
  • Resilience — one broken link doesn't stop the whole batch
  • Repeatability — rerun it anytime with a new list of URLs

The Script

Here's the full implementation:

import os
import requests

def download_image(url, folder="downloaded_images", filename=None):
    """
    Downloads a single image from a given URL and saves it to a folder.
    """
    os.makedirs(folder, exist_ok=True)

    if filename is None:
        filename = url.split("/")[-1].split("?")[0]
        if not filename:
            filename = "image.jpg"

    filepath = os.path.join(folder, filename)

    try:
        response = requests.get(url, stream=True, timeout=10)
        response.raise_for_status()

        with open(filepath, "wb") as f:
            for chunk in response.iter_content(chunk_size=8192):
                f.write(chunk)

        print(f"✅ Downloaded: {filepath}")
        return filepath

    except requests.exceptions.RequestException as e:
        print(f"❌ Failed to download {url}: {e}")
        return None


def download_multiple_images(url_list, folder="downloaded_images"):
    """
    Downloads multiple images from a list of URLs.
    """
    results = []
    for idx, url in enumerate(url_list, start=1):
        print(f"Downloading image {idx}/{len(url_list)}...")
        result = download_image(url, folder=folder)
        results.append(result)
    return results


if __name__ == "__main__":
    # Replace with your own list of image URLs
    image_urls = [
        "https://example.com/image1.jpg",
        "https://example.com/image2.png",
    ]

    download_multiple_images(image_urls)
Enter fullscreen mode Exit fullscreen mode

Breaking It Down

1. Streaming the download

response = requests.get(url, stream=True, timeout=10)
Enter fullscreen mode Exit fullscreen mode

Setting stream=True means the file is downloaded in chunks instead of loading the entire image into memory at once. This matters more than you'd think once you start dealing with larger files — it keeps memory usage flat regardless of image size.

The timeout=10 is a small but important detail: without it, a single unresponsive server can hang your script indefinitely.

2. Writing in chunks

with open(filepath, "wb") as f:
    for chunk in response.iter_content(chunk_size=8192):
        f.write(chunk)
Enter fullscreen mode Exit fullscreen mode

Instead of writing the whole response body in one go, the script writes 8KB chunks as they arrive. This is the standard pattern for downloading binary files in requests and works well for anything from tiny thumbnails to multi-megabyte images.

3. Graceful failure

except requests.exceptions.RequestException as e:
    print(f"❌ Failed to download {url}: {e}")
    return None
Enter fullscreen mode Exit fullscreen mode

RequestException is the base class for pretty much everything that can go wrong with a requests call — connection errors, timeouts, bad status codes (via raise_for_status()), and so on. Catching it here means one dead link just gets logged and skipped, rather than crashing the whole batch.

4. Auto-naming files

filename = url.split("/")[-1].split("?")[0]
Enter fullscreen mode Exit fullscreen mode

This grabs the last segment of the URL path and strips off any query string, so a URL like https://example.com/photos/sunset.jpg?size=large becomes sunset.jpg. It's not bulletproof (some URLs won't have a clean filename at the end), but it covers the common case, and you can always pass an explicit filename if you need more control.

Using It

Drop your URLs into a list and run it:

image_urls = [
    "https://example.com/image1.jpg",
    "https://example.com/image2.png",
]

download_multiple_images(image_urls)
Enter fullscreen mode Exit fullscreen mode

Every image gets saved into a downloaded_images/ folder in your working directory, created automatically if it doesn't already exist.

Requirements

Just one dependency:

pip install requests
Enter fullscreen mode Exit fullscreen mode

Where You Could Take This Further

This script covers the basics, but there's room to grow it depending on what you need:

  • Parallel downloads — using concurrent.futures.ThreadPoolExecutor or asyncio + aiohttp to download many images at once instead of sequentially
  • Content-type validation — checking the response's Content-Type header to confirm you actually got an image before saving it
  • Retry logic — wrapping the request in a retry decorator for flaky connections
  • Progress bars — plugging in tqdm for a nicer CLI experience on large batches
  • Deduplication — hashing file contents to skip images you've already downloaded

Wrapping Up

It's a small script, but it's a good example of how a few well-placed lines — streaming, chunked writes, and proper exception handling — turn a "works on my machine for one file" snippet into something you can actually trust to run unattended on a list of hundreds of URLs.

If you end up extending it (especially the async version), I'd love to hear about it in the comments!

Top comments (0)