mrkeyoor.com_
Thu 06 Aug 15:39 UTC
PyPIUtilsupdated 06 Aug 2026

filetype

filetype tells you what a file actually is by reading its magic number, the fixed byte signature at the start of the file, instead of trusting the extension or the Content-Type header a client sent you. It is a straight port of the Go library of the same name, it is pure Python with zero dependencies and no C extensions, and it only ever looks at the first 8192 bytes, so you can hand it a path, a bytes buffer, a memoryview or an open file object and it will not read a two gigabyte upload into memory. It recognises around a hundred binary formats across images, video, audio, archives, Office and OpenDocument files, fonts and wasm, and returns an object with .mime and .extension or None when nothing matches.

Verdict

The right first line of defence for binary uploads when you want zero dependencies and a two-line integration, and it does that job well. Just do not mistake a hundred magic numbers for real content inspection, and be aware that the last release predates the last commit by more than two years.

API stability5/5guess, guess_mime, guess_extension and the is_* helpers have not changed shape since 1.0, and with no release since November 2022 there has been nothing to break. Stability here is partly a side effect of inactivity.
Docs3/5The README covers the common calls and lists every supported type with its MIME string, and there is a generated API reference at h2non.github.io. What is missing is accuracy on the header size and any guidance on the failure modes, such as text files or zip-based documents returning nothing useful.
Maintenance2/5Last PyPI release was 1.2.0 in November 2022 while the repo saw commits through May 2025, so accepted fixes are unreleased. 53 open issues stand against a single-author project whose author maintains many other packages.
Ecosystem4/5About 10.8M downloads a week, largely as a transitive dependency inside upload-handling and document-processing stacks. It is widely used but rarely written about, so answers come from reading the type matchers rather than from tutorials.

Use it if

  • You accept file uploads and need to reject a .jpg that is really a zip or an executable; the browser-supplied filename and Content-Type are both attacker-controlled and this is the cheap server-side check
  • You want no C dependency: python-magic needs libmagic installed on the host, which turns a pip install into a Dockerfile change and a Windows headache, and filetype has literally no dependencies
  • You are working from a stream or an S3 range request and only have the first few kilobytes: pass those bytes directly and get an answer without downloading the rest
  • You need to sort a pile of files into images, video, audio, archives, documents or fonts, which the is_image / is_video / is_document helpers do in one call
Skip it if

Setup reality

pip install filetype and you are done, which is genuinely the whole point of picking it over python-magic. Two things in the README are worth correcting before you design around them. It says only the first 261 bytes are needed, but the code sets _NUM_SIGNATURE_BYTES to 8192 and reads that much, because zip-based formats like docx and epub need to see entry names further in; budget 8KB, not 261 bytes, when fetching a range from object storage. It also does not declare requires_python and its classifiers stop at Python 3.9, so pip will happily install it on 3.13 where it does work, but you are outside anything the metadata promises. One more sharp edge: filetype.add_type() mutates a module-level list, so a custom matcher registered anywhere in your process affects every other caller, including library code you did not write.

Patterns

Identify a file on diskguess-file-type

import filetype

kind = filetype.guess("uploads/photo.bin")
if kind is None:
    print("unknown or text-based file")
else:
    print(kind.extension)  # 'jpg'
    print(kind.mime)       # 'image/jpeg'

None means either an unrecognised binary or any text format, and the library cannot tell you which. A missing path raises FileNotFoundError rather than returning None, so guard the path first.

Identify from a bytes buffer you already holdguess-from-bytes

import filetype

header = response.content[:8192]
kind = filetype.guess(header)
print(filetype.guess_mime(header))
print(filetype.guess_extension(header))

8192 is the real limit the library reads, despite the README's 261 bytes. Passing fewer bytes than that makes zip-based formats such as docx and epub fall back to a plain zip match.

Reject an upload whose real type does not match its claimvalidate-upload

import filetype

ALLOWED = {"image/jpeg", "image/png", "image/webp"}

def check(upload_file):
    kind = filetype.guess(upload_file.file)   # file-like object
    if kind is None or kind.mime not in ALLOWED:
        raise ValueError(f"rejected: detected {kind.mime if kind else 'unknown'}")
    return kind.mime

Never trust upload_file.content_type; the client sets it. Passing the file object works because filetype records tell(), seeks to 0, reads 8192 bytes and restores the position, so your later read still starts where you left it.

Ask what family a file belongs tocheck-file-category

import filetype

if filetype.is_image(buf):
    store_thumbnail(buf)
elif filetype.is_video(buf):
    queue_transcode(buf)
elif filetype.is_archive(buf):
    reject("archives not allowed")
elif filetype.is_document(buf):
    queue_text_extraction(buf)

The helpers are is_image, is_video, is_audio, is_archive, is_font and is_document. There is no is_application helper even though wasm is in the type table; use filetype.application_match(buf) for that group.

Only run the matchers you care aboutmatch-within-one-group

from filetype import image_match, video_match, match
from filetype.types import IMAGE

kind = image_match(buf)      # skips ~80 non-image matchers

# or pass an explicit matcher list
kind = match(buf, matchers=IMAGE)

match() walks the type list in order and returns the first hit, so restricting the list is both faster and safer: it removes any chance of a stray archive or font signature winning on a crafted buffer.

Register a signature the library does not knowadd-custom-matcher

import filetype
from filetype.types import Type

class Parquet(Type):
    MIME = "application/vnd.apache.parquet"
    EXTENSION = "parquet"

    def __init__(self):
        super().__init__(mime=Parquet.MIME, extension=Parquet.EXTENSION)

    def match(self, buf):
        return len(buf) > 3 and buf[:4] == bytearray([0x50, 0x41, 0x52, 0x31])

filetype.add_type(Parquet())
print(filetype.guess_extension(parquet_bytes))  # 'parquet'

add_type inserts at position 0 of a module-level list, so your matcher runs before every built-in and the change is process-wide. Registering the same type twice on a hot reload silently stacks duplicates.

Find a type entry by MIME or extensionlookup-type-by-mime

import filetype

kind = filetype.get_type(mime="image/avif")
print(kind.extension if kind else "unsupported")

print(filetype.is_mime_supported("video/webm"))     # True
print(filetype.is_extension_supported("svg"))       # False, SVG is text

get_type matches on either argument with an or, so passing a mime that does not exist together with an extension that does still returns a hit. Pass one argument at a time if you want a strict lookup.

Enumerate everything the library can detectlist-supported-types

import filetype

for kind in filetype.types:
    print(kind.extension, kind.mime)

print(len(filetype.types))

This is the honest way to decide whether the library covers your inputs, and it takes ten seconds. Run it before assuming support for anything text-based, since the list is entirely binary formats.

Identify a remote object without downloading itdetect-from-remote-range

import boto3, filetype

s3 = boto3.client("s3")
head = s3.get_object(Bucket="uploads", Key=key, Range="bytes=0-8191")["Body"].read()

kind = filetype.guess(head)
print(kind.mime if kind else "unknown")

Request 0-8191, not 0-260: the shorter range is what the README suggests and it breaks Office and OpenDocument detection. This costs one small GET instead of pulling a multi-gigabyte object through your process.

Understand why docx sometimes reports as ziphandle-office-documents

import filetype, zipfile, io

kind = filetype.guess(buf)
if kind and kind.extension == "zip":
    # possible OOXML that the entry-order heuristic missed
    with zipfile.ZipFile(io.BytesIO(buf)) as z:
        names = set(z.namelist())
    if "word/document.xml" in names:
        kind_mime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"

The OOXML matcher looks for [Content_Types].xml, _rels/.rels or docProps among the first few zip entries inside 6000 bytes. Writers that order entries differently, or that store the document deeper, produce a plain zip result and need the fallback above (which requires the whole file, not just the header).

Guard against empty and falsy inputempty-input-behaviour

import filetype

print(filetype.guess(b""))     # None, short-circuited before matching
print(filetype.guess(None))    # None

try:
    filetype.guess(12345)
except TypeError as e:
    print(e)   # Unsupported type as file input: <class 'int'>

guess() returns None for anything falsy without running a single matcher, so an empty upload looks the same as an unrecognised one. Check length yourself if zero-byte files need a distinct error.

Alternatives

PackageRegistryPick it when
python-magicPyPIYou need libmagic's thousands of signatures, text format detection or charset guessing and can install a system library.
puremagicPyPIYou want dependency-free detection like filetype but a larger signature table and confidence-ranked results.
magikaPyPIYou need text and source-code formats identified too and can afford a bundled model rather than byte signatures.