CLI tool
ianare/exif-py avatar
ianare/exif-py

ExifRead: pulling camera metadata out of images with plain Python

Easy to use Python module to extract Exif metadata from digital image files.

973 stars215 forksPythonBSD-3-Clause

At a glance

What is it?
ExifRead parses EXIF, GPS, thumbnail and MakerNote tags from JPEG, TIFF, PNG, WebP, HEIC and RAW files using nothing but the Python standard library, which makes it a good fit for scripts and services that need image metadata without an imaging stack.
Who is it for?
ExifRead fits pipelines that only need metadata: asset importers, upload validators, bulk renamers, GPS extractors and thumbnail-free batch jobs. It is the wrong tool when you need pixels decoded and transformed, or when you need to write metadata back into a file, which the documented API does not cover.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 135 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why parse EXIF without an imaging stack

Most EXIF tooling arrives bundled with a compiler, an image decoding library, or a full imaging framework, and that weight is often wasted when the only goal is to read a dictionary of tags. ExifRead, published on PyPI under the name ExifRead and imported as exifread, takes the opposite route. The project describes itself as pure Python, lightweight, and without dependencies, and the packaging metadata backs that up: the dependency list in pyproject.toml is empty, the build backend is plain setuptools, and the package requires Python 3.7 or newer. The only runtime requirement is the interpreter itself.

That constraint is the point. A photo upload service that wants to reject images missing a GPS tag, a batch script that wants to rename files by capture date, or a data pipeline that must not install binary wheels on an unfamiliar platform can add this parser without pulling a native build chain into the image. Supported containers are listed as TIFF, JPEG, JPEG XL, PNG, WebP, HEIC and RAW, which covers the formats that arrive from phones, cameras, scanners and screen-capture tools.

The audience is engineers writing short utilities, not photographers looking for a desktop viewer. The library ships a command line entry point named EXIF.py and a module API that returns a plain dictionary, so both one-off inspection and integration into a larger service are supported paths. The README also states that the software is mature and aims for stability, and the PyPI classifiers in pyproject.toml mark the project as Production/Stable, though the release history itself is quiet: the three most recent versions, 3.4.0, 3.5.0 and 3.5.1, are all dated in August 2025, and the last push to the repository was on 2026-06-10.

The tag dictionary, its IFD prefixes and its MakerNote layer

The core of the library is a single call that turns a binary file handle into a dictionary of tags. The mapping is flat, and every key is the name of an image file directory (IFD) followed by the tag name, which makes results easy to filter but also means you cannot ask for a tag without knowing which directory it lives in. The README gives the examples 'EXIF DateTimeOriginal', 'Image Orientation' and 'MakerNote FocusMode', so a capture timestamp arrives under the EXIF prefix while orientation arrives under Image.

Those prefixes map onto the standard structure of EXIF data. Image refers to IFD0, the main image directory. Thumbnail refers to IFD1, the embedded thumbnail directory. EXIF, GPS and Interoperability are sub-directories. MakerNote is the awkward one: it holds manufacturer specific data, and the README states plainly that there are no official published references for those tags. Any parser therefore has to ship tables of vendor offsets, and those tables are where two readers of the same file can disagree. ExifRead leans on that uncertainty by treating MakerNote as a separable processing step, so a caller who does not need those tables can leave them out of the way.

The second layer of detail is that values are not plain Python objects by default. The library returns IfdTag objects, which carry a human readable print representation, a description and a values member. A single tag can therefore hold several values, and code that wants the raw orientation numbers has to reach into the tag rather than treat it as a scalar. Later versions added an option to return built-in types instead, which is the single most useful change for anyone who wants to serialize results.

Installing ExifRead and reading the first file

The recommended install is the PyPI package, and the README notes that going through PyPI makes it easy to stay current:

bash
pip install exifread

The package installs a console script called EXIF.py, because pyproject.toml maps the EXIF.py entry point to exifread.cli:main. A first look at a single file is one command:

bash
EXIF.py image1.jpg

Passing more than one file, and adding -dc to print the tag descriptions as well, gives a fuller dump:

bash
EXIF.py -dc image1.jpg image2.tiff

The options themselves are discoverable from the tool:

bash
EXIF.py -h

From a script, the call is deliberately small. The file has to be opened in binary mode, which is a detail worth internalising because a text-mode open raises before any parsing happens:

python
import exifread

# Open image file for reading (must be in binary mode)
with open(file_path, "rb") as file_handle:

    # Return Exif tags
    tags = exifread.process_file(file_handle)

The result is a dictionary of tag names to values, and the README shows the usual way to walk it while skipping the bulky entries such as the embedded thumbnails:

python
for tag, value in tags.items():
    if tag not in ('JPEGThumbnail', 'TIFFThumbnail', 'Filename', 'EXIF MakerNote'):
        print(f"Key: {tag}, value {value}")

The excluded keys are worth noting on their own. JPEGThumbnail and TIFFThumbnail are full image payloads, so a naive loop over the dictionary will dump binary blobs into a log file. Filename is bookkeeping added by the library, and EXIF MakerNote is exactly the vendor blob the filter is there to keep out of the way.

The processing switches that decide how much work gets done

The same options exist in the command line and in the function, which is unusual and useful when a batch job and a manual inspection need to agree. There are four that matter in practice.

The quick mode, -q or --quick, disables MakerNote processing and thumbnail extraction. In code the same thing is done by passing details=False and extract_thumbnail=False:

python
tags = exifread.process_file(
    file_handle, details=False, extract_thumbnail=False
)

The two halves can be set independently, so a caller who wants the MakerNote tables but not the thumbnail payload passes details=True, extract_thumbnail=False, and the reverse combination details=False, extract_thumbnail=True also works. The README points out that the first and the stop-tag option are useful for speed on large numbers of files, which matters if you are walking a photo library rather than inspecting one picture.

Stopping early is the second lever. The -t or --stop-tag argument takes a tag name without the IFD prefix, for example DateTimeOriginal, and parsing halts once that tag is found:

python
tags = exifread.process_file(file_handle, stop_tag='TAG')

The third option is strictness. By default an invalid tag is silently ignored; passing -s or --strict turns that into an error instead:

python
tags = exifread.process_file(file_handle, strict=True)

That default is a deliberate trade-off. Silent tolerance is what lets a batch job survive a file with a corrupt tag, but it also means a broken file looks identical to a file with no metadata. Strict mode exists for callers who would rather see a failure.

The fourth is type conversion, and it changes how the returned values behave. With builtin_types=True the dictionary holds int, float, str, bytes, list or None instead of IfdTag objects, which removes the need to reach into the values member for every tag. Combined with details=False, the result contains no bytes and can go straight to a JSON encoder:

python
json.dumps(
    exifread.process_file(file_handle, details=False, builtin_types=True)
)

The remaining caveat is the thumbnail payload, which is bytes by nature and stays bytes even in built-in type mode.

Where ExifRead stops short: pixels, thumbnails and writing metadata

The limits are clear once the scope is stated. ExifRead is a reader, not an editor. The documented API returns tags, and nothing in the documented usage writes EXIF data back into an image, so a project that needs to strip GPS coordinates or set a copyright field by rewriting the file will not find that capability here. A pure Python TIFF and JPEG writer is a substantially different piece of work from a tag reader.

It also does not decode pixels. The README's own example of correcting image orientation uses Pillow to perform the transformation, and ExifRead only supplies the orientation value. Reading a 24 megapixel RAW file therefore does not give you a usable image, only its metadata, and the file handles are opened in binary mode with no image buffer anywhere in the flow.

Thumbnail handling is a specific trap. When extract_thumbnail is left on, the dictionary contains JPEGThumbnail or TIFFThumbnail entries holding the embedded preview bytes. Iterating and printing the dictionary without filtering, as the README example filters, will push binary content into your terminal or log. Two smaller constraints follow from the same design: the MakerNote tables are keyed to specific vendors, so a tag that your phone writes may be missing or named differently than in the README examples, and the strict option raises on an invalid tag rather than returning a partial result, so a single corrupt file can abort a long run if you enable it.

For developers who want a development install rather than the PyPI release, the Makefile offers a path that creates a virtual environment and installs the development extras. The README gives the target as:

bash
make venv install-all

The Makefile also shows how the project tests itself, running the CLI across the images under tests/resources and diffing the output against a stored dump, which is a reasonable model if you plan to patch tag tables yourself.

How ExifRead compares with Pillow and with a full metadata stack

The realistic comparison is with imaging libraries that expose EXIF as a side effect of decoding an image. Pillow, which appears in the orientation example, is used there to open the file and apply the transformation, while ExifRead supplies only the orientation value. That split is the practical difference: the pixel work and the tag reading are separate steps, and you only pay for a decode when you actually want one. ExifRead is built the other way round: metadata first, pixels never, and a file format list that includes HEIC, JPEG XL and RAW variants.

The other axis is breadth. Metadata tooling that handles XMP, IPTC and IPTC-IIM in addition to EXIF, such as dedicated EXIF command line utilities or larger catalogue programs, covers more tag families than a library that concentrates on EXIF and MakerNote. The repository topics list includes xmp among the keywords, but the documented tag categories in the README are Image, Thumbnail, EXIF, GPS, Interoperability and MakerNote, and there is no documented API for reading XMP payloads as structured objects. If your data source stores its descriptive metadata in XMP rather than EXIF, that difference decides the question before anything else does.

The size argument is the one that survives scrutiny. A dependency-free parser installs in seconds on any CPython 3.7 to 3.13 host, needs no system libraries such as libjpeg, and its tags are returned as ordinary dictionary entries, so the output drops into existing logging, reporting and API code without a custom serialisation layer. A library that decodes images offers more, but brings a native dependency and a different mental model into a job that only ever needed the tags.

Licence, versioning and what an upgrade actually costs

The licence is BSD-3-Clause, stated in the project metadata and shipped in a LICENSE file at the repository root. That is a permissive licence with no copyleft obligation: it permits use inside proprietary and closed-source products, and it requires that the copyright notice and the licence text travel with redistributed copies. The README shows the standard badge for it. This is a description of the licence terms as published, not legal advice, and organisations with their own review process will still want to read the file in the repository themselves.

Versioning is simple. The version is read from exifread.__version__ through setuptools dynamic metadata rather than being hard coded in pyproject.toml, and the published releases 3.4.0, 3.5.0 and 3.5.1 arrived in quick succession on 2025-08-04 and twice on 2025-08-23. The project ships a ChangeLog.rst and a CONTRIBUTING.rst at the top level, and the pyproject.toml points users at the changelog on GitHub. A conventional bump of a minor version, with tag names and a changelog entry, means an upgrade is a routine dependency bump, and the README's stability claim is consistent with that shape of release history.

The upgrade cost of adopting the library is mostly in your own code. Moving from IfdTag objects to built-in types changes what you read out of the dictionary, so a project that first adopted the library on the object API will need to revisit access patterns, most obviously the orientation example, which reads orientation.values. The tests directory and the stored dump file in the Makefile suggest the project itself relies on comparing output text, so tag ordering or formatting changes would be visible in a diff, which is a useful thing to know before you pin or vendor the package.

Editorial conclusion

ExifRead fits pipelines that only need metadata: asset importers, upload validators, bulk renamers, GPS extractors and thumbnail-free batch jobs. It is the wrong tool when you need pixels decoded and transformed, or when you need to write metadata back into a file, which the documented API does not cover. Before adopting it, check the tags your own camera or phone writes, because MakerNote coverage is the part most likely to vary by vendor, and confirm that the Python version you deploy (3.7 to 3.13 are supported) matches your runtime.

Frequently asked questions

What image formats can ExifRead read?

The README lists TIFF, JPEG, JPEG XL, PNG, WebP, HEIC and RAW as supported formats. The parser works on the metadata structures those files carry, and it does not decode the image pixels.

How do I read EXIF tags from a file in Python with ExifRead?

Open the file in binary mode and pass the handle to exifread.process_file, which returns a dictionary of tag names to values. Keys combine the IFD name with the tag name, for example 'EXIF DateTimeOriginal' or 'Image Orientation'.

How can I speed up ExifRead when processing many files?

Pass details=False and extract_thumbnail=False to skip MakerNote processing and thumbnail extraction, or set stop_tag to a tag name such as 'DateTimeOriginal' so parsing halts once that tag is found. Both are available as command line switches, -q and -t.

Can ExifRead output JSON directly?

The built-in types option returns int, float, str, bytes, list or None instead of IfdTag objects. Combined with details=False, which avoids bytes in the result, the dictionary can be passed straight to json.dumps.

Official sources

  1. ianare/exif-py on GitHub
  2. Issues
  3. License: BSD-3-Clause
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ianare-exif-py.svg)](https://hysenlabs.com/projects/ianare-exif-py)

Was this useful?