If you’ve ever been in charge of a blog, you know how annoying it is to handle images.
Unless you want your website to become a slow, bloated piece of garbage over time, you need to resize and optimize them.
And if you’re dealing with a blog that’s focused on visuals, this can take ages. Tools like TinyJPG help, but you still have to resize them manually and it has an upload limit.
Python, however, can speed things up a lot.
In this article, we explore how to use Python to automatically resize and compress images so they’re the best quality for your blog.
How to optimize images using Python
First of all, we’ll need to import some modules so we can work with images and find all the files we need. We’ll be using the following modules:
- Pillow, to manipulate the images.
- Glob, to gather all the files.
- OS, to create folders.
- Path, to work with file paths.
#imports import glob import os from PIL import Image from pathlib import Path
Because we don’t want to edit the script every time we have a new folder with images, we’re using user input to ask for the source folder. We’re also asking the user for the maximum width of the images and whether or not it needs to overwrite previously optimized images. If the images just need to be optimized, we want to leave the max-width empty.
Since I’m the only one using this, I’m not building in any validation of inputs.
file_types = ('.jpg','.png', '.jpeg') all_images = [] in_folder = input("Enter the path to the folder containing the images:") out_folder = in_folder + '/Optimized' # If we just want to optimize the images without resizing them, we can # leave the max width input empty try: img_max_width = int(input("How wide do you want the images to be (in px):")) except ValueError: img_max_width = 0 overwrite_img = input("Overwrite previously optimized images? Y/N:")
Instead of putting all our optimized images in the same folder as our unoptimized images, we’re creating a folder called Optimized
. If it already exists, print a statement saying it already exists.
#Create ouput folder try: os.mkdir(out_folder) print('Folder created: ' + str(out_folder)) except FileExistsError: print('Folder already exists: ' + str(out_folder))
Next, we’ll scan our input folder and add any .png
, .jpg
, or .jpeg
files to our image array.
#Get all images from input folder for file_type in file_types: all_images.extend(glob.glob(in_folder + '/*' + file_type))
Now the real work begins. For each image in our array we’ll do the following:
- Get the file path of the image
- Construct our output file path
- Check if the output file (the optimized image) already exists so we can skip it if we don’t want to overwrite images
- Resize the image to our specifications
# Resize all files and optimize # Enumerate the list so we can track what image it is on for index, img in enumerate(sorted(all_images)): img_path = Path(img) file_name = img_path.name outF = out_folder + '/' + file_name #Communicate what image we're working on print('Working on optimizing ' + str(img)) # Check if the file exists and if we want to overwrite it. # If not, move on to the next image if Path(outF).is_file() and overwrite_img.upper() == "N": print('Image already optimized, skipping...') continue else: with Image.open(img) as inF: orig_width, orig_height = inF.size if img_max_width is 0: inF.thumbnail((orig_width,orig_height), reducing_gap= 2.0) inF.save(outF,inF.format) else: #Calculate ratio so we can keep the height proportionate to the width ratio = inF.size[0] / img_max_width inF.thumbnail((img_max_width,inF.size[1]*ratio), reducing_gap= 2.0) inF.save(outF,inF.format) print(str(index + 1) + ' out of ' + str(len(all_images)) + ' images done')
We can change the reducing_gap
to fine-tune the quality. But for now, we’re going to leave it at 2.
And that’s the script. But because I don’t want to open the command line, go to the source folder, and run the script every time I need to optimize images, we’ll create a .bat
file on the desktop.
C:/PathToPython/python.exe "D:/Path/To/Python/Script/ImageOptimizer.py"
Now that we have this, we just have to run the .bat
file, put in the path to the folder with all our images, put in the max-width of our images, and if we want to overwrite existing ones. Hooray for technology!
How well does this compress images?
To test how well this script optimizes images, I’m going to optimize 10 images using both TinyJPG and this script.
To prep this test, I’ve taken 10 images from Unsplash (1,2,3,4,5,6,7,8,9,10) and scaled them down to 700px wide using Affinity Photo.
Our script can scale the images down as well, but it wouldn’t be a good test.
Image | Original size | After TinyJPG | After our script |
---|---|---|---|
Image 1 | 536 KB | 74 KB | 77 KB |
Image 2 | 320 KB | 62 KB | 45 KB |
Image 3 | 474 KB | 63 KB | 52 KB |
Image 4 | 627 KB | 251 KB | 108 KB |
Image 5 | 1182 KB | 425 KB | 168 KB |
Image 6 | 335 KB | 66 KB | 52 KB |
Image 7 | 566 KB | 108 KB | 73 KB |
Image 8 | 234 KB | 70 KB | 23 KB |
Image 9 | 681 KB | 121 KB | 88 KB |
Image 10 | 395 KB | 104 KB | 62 KB |
Total savings | 4006 KB | 4602 KB |
The file size is one thing, but how’s the quality of our optimized images? To compare, I’m using the image we compressed the most (image 5).

From afar, there isn’t much of a difference — and you could argue that’s good enough.
Once you zoom in, though, our script shows a slight drop in quality. The trees get a bit blurry after using our script.
To fix this, we could increase the reducing_gap
to a level where the drop is less noticeable.
But that’s our script. It has saved me a ton of time having to manually resize the images we use at Piktochart. If you want the full script, you can find it below:
Full code:
#imports import glob import os from PIL import Image from pathlib import Path def main(): file_types = ('.jpg','.png', '.jpeg') all_images = [] in_folder = input("Enter the path to the folder containing the images:") out_folder = in_folder + '/Optimized' # If we just want to optimize the images without resizing them, we can # leave the max width input empty try: img_max_width = int(input("How wide do you want the images to be (in px):")) except ValueError: img_max_width = 0 overwrite_img = input("Overwrite previously optimized images? Y/N:") #Create ouput folder try: os.mkdir(out_folder) print('Folder created: ' + str(out_folder)) except FileExistsError: print('Folder already exists: ' + str(out_folder)) #Get all images from input folder for file_type in file_types: all_images.extend(glob.glob(in_folder + '/*' + file_type)) #Communicate how many images are found so we can double check manually if needed print(str(len(all_images)) + ' image(s) found') # Resize all files if needed and optimize # Enumerate the list so we can track what image it is on for index, img in enumerate(sorted(all_images)): img_path = Path(img) file_name = img_path.name outF = out_folder + '/' + file_name #Communicate what image we're working on print('Working on optimizing ' + str(img)) # Check if the file exists and if we want to overwrite it. # If not, move on to the next image if Path(outF).is_file() and overwrite_img.upper() == "N": print('Image already optimized, skipping...') continue else: with Image.open(img) as inF: orig_width, orig_height = inF.size if img_max_width == 0: inF.thumbnail((orig_width,orig_height), reducing_gap= 2.0) inF.save(outF,inF.format) else: #Calculate ratio so we can keep the height proportionate to the width ratio = inF.size[0] / img_max_width inF.thumbnail((img_max_width,inF.size[1]*ratio), reducing_gap= 2.0) inF.save(outF,inF.format) print(str(index + 1) + ' out of ' + str(len(all_images)) + ' images done') if __name__ == '__main__': main()