python 91 lines · 3 tabs

Validating Upload Size and MIME Type in a Custom Django Form Field

Shared by codesnips Aug 2026
3 tabs
import magic
from django import forms
from django.core.exceptions import ValidationError
from django.template.defaultfilters import filesizeformat


class RestrictedFileField(forms.FileField):
    def __init__(self, *args, **kwargs):
        self.max_size = kwargs.pop("max_size", None)
        self.content_types = kwargs.pop("content_types", None)
        self.read_bytes = kwargs.pop("read_bytes", 2048)
        super().__init__(*args, **kwargs)

    def clean(self, data, initial=None):
        file = super().clean(data, initial)
        if not file:
            return file

        if self.max_size is not None and file.size > self.max_size:
            raise ValidationError(
                "File is too large (%(size)s). Maximum size is %(max)s.",
                code="file_too_large",
                params={
                    "size": filesizeformat(file.size),
                    "max": filesizeformat(self.max_size),
                },
            )

        if self.content_types:
            head = file.read(self.read_bytes)
            file.seek(0)  # rewind so storage reads the full file later
            detected = magic.from_buffer(head, mime=True)

            if detected not in self.content_types or (
                file.content_type and file.content_type not in self.content_types
            ):
                raise ValidationError(
                    "Unsupported file type: %(type)s.",
                    code="unsupported_type",
                    params={"type": detected},
                )

        return file
3 files · python Explain with highlit

This snippet shows how upload validation belongs on a custom form field rather than scattered across views, so every form that accepts a document gets the same size and content-type checks for free. The core idea is that Django's forms.FileField already returns an UploadedFile in its clean(), and subclassing it lets extra rules run inside the normal validation pipeline, raising ValidationError that renders inline next to the widget.

In fields.py, RestrictedFileField accepts max_size and content_types in its constructor and stores them as instance state. Its overridden clean() first defers to super().clean() so the built-in required/empty handling stays intact, then returns early when the field is optional and empty. The size guard uses file.size and filesizeformat to build a human-readable message, which is friendlier than a raw byte count.

The important subtlety is MIME sniffing. The browser-supplied content_type is attacker-controlled and trivially spoofed, so RestrictedFileField reads the first read_bytes of the stream and passes them to python-magic, which inspects the actual file signature. After sniffing, file.seek(0) rewinds the pointer so downstream code and storage backends still read the whole file. Both the declared and the detected type are checked, closing the gap where a renamed .exe claims to be a PDF.

In forms.py, DocumentUploadForm wires the field with concrete limits — a 5 MB cap and an allowlist of PDF and common image types — demonstrating that policy lives in the form definition, not the field. Because the field is self-contained, the same configuration can be reused across multiple forms.

In views.py, upload_document is a thin FormView; by the time form_valid() runs, the file has already passed every check, so the view simply persists cleaned_data['attachment']. Note the trade-off: reading bytes for sniffing touches the upload in memory or temp storage, and very large files should be streamed rather than fully buffered. python-magic also depends on the system libmagic, a deployment consideration worth documenting. This pattern is the right reach whenever untrusted uploads must be constrained before they hit disk or a task queue.


Related snips

ruby
payload = {
  sub: user.id,
  iss: 'https://auth.example.com',
  aud: 'codesnips-api',
  exp: 15.minutes.from_now.to_i,
  iat: Time.now.to_i,

JWT issuance and verification without common footguns

jwt authentication api
by Kai Nakamura 2 tabs
python
class Product(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField(blank=True)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    cost = models.DecimalField(max_digits=10, decimal_places=2)
    margin = models.DecimalField(max_digits=5, decimal_places=2, blank=True)

Django model signals vs overriding save

django python models
by Priya Sharma 2 tabs
ruby
class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :account_name, :string
  attribute :email, :string

Shallow Controller, Deep Params: Form Object Pattern

rails activemodel form-object
by codesnips 3 tabs
python
from django.urls import path
from . import views

app_name = 'blog'

urlpatterns = [

Django URL namespacing and reverse lookups

django python urls
by Priya Sharma 3 tabs
bash
#!/usr/bin/env bash
set -euo pipefail

export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"

Secrets management with environment isolation and Vault

secrets-management vault environment-variables
by Kai Nakamura 1 tab
typescript
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";

const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";

JWT access + refresh token rotation (conceptual)

security node jwt
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Validating Upload Size and MIME Type in a Custom Django Form Field — share card
Link copied