Handling File Uploads
A file upload lets a user send a file — an image, PDF, or document — from their browser to your Flask app through an HTML form.
Learn Handling File Uploads in our free Flask course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Flask course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
In this lesson you'll wire up the right form, read uploads from request.files , sanitize filenames with secure_filename , save files to disk, and lock things down by validating the file type and capping the upload size.
File uploads travel in a special kind of POST request. For the bytes of the file to actually reach your server, the HTML form must declare enctype="multipart/form-data" and use method="POST" . The form also needs an <input type="file" name="file"> — that name is the key you'll look up on the server.
On the Flask side, uploaded files appear in request.files , a dictionary-like object. Each value is a FileStorage object that exposes .filename (the original name the browser sent), .read() (the raw bytes), and .save(path) (write it to disk).
Always check that the file is present and that a file was actually chosen before you use it — an empty filename means the user submitted the form without picking anything. The example below validates both conditions, then reports the name and byte count.
We use io.BytesIO to fake a file in memory so the example runs without touching your disk. A real browser would send the same multipart request when the user picks a file and submits the form.
Never trust a filename from the browser. A malicious user could submit a name like ../../etc/passwd to try to escape your upload folder, or upload an executable disguised with a misleading name. Two defenses handle this: sanitize the name and validate the extension .
secure_filename from werkzeug.utils strips dangerous path parts and odd characters, turning ../my report.txt into a safe my_report.txt . To restrict the kinds of files you accept, keep an allow-list of extensions and reject anything outside it before saving. The example below does both and rejects an .exe upload outright.
Checking the extension is a cheap first line of defense, but it only inspects the name, not the actual contents. For higher-stakes apps you'd also verify the file's real type — but an allow-list plus secure_filename already blocks the most common mistakes.
Without a size cap, a single user could upload a multi-gigabyte file and exhaust your server's disk or memory. Flask gives you a one-line guard: set app.config["MAX_CONTENT_LENGTH"] to the maximum number of bytes you'll accept.
When a request body exceeds that limit, Flask rejects it automatically with a 413 Request Entity Too Large response — before your view function ever runs. The demo below uses a deliberately tiny limit so you can watch a small upload succeed and a larger one get blocked.
The small upload returns 200 ; the 500-byte upload returns 413 because its multipart body sails past the 256-byte cap. Note the limit covers the whole request body, not just the file's raw bytes, so pick a value with a little headroom.
A safe upload route does all five: right enctype, read from request.files , sanitize, validate the extension, and cap the size.
Complete the route below. Replace each ___ so it reads the uploaded file, checks that one was chosen, and returns its name and size in bytes.
❌ Forgetting the enctype (file arrives empty)
If your form lacks enctype="multipart/form-data" , the browser sends only the filename as text, so request.files is empty and your upload looks like it vanished. Add the enctype and use method="POST" .
The upload exceeded MAX_CONTENT_LENGTH , so Flask rejected it before your view ran. Raise the limit if the file is legitimately large, or tell the user the maximum size up front.
Build a route that accepts a .csv upload and reports how many rows it contains.
Lesson complete — you can accept files safely!
You can wire up a multipart/form-data form, read uploads from request.files , sanitize names with secure_filename , save files to disk, validate extensions, and cap the upload size with MAX_CONTENT_LENGTH .
🚀 Up next: Configuration & Environments — manage settings cleanly across development and production.
Practice quiz
Where do uploaded files appear in a Flask request?
- request.form
- request.files
- request.args
- request.data
Answer: request.files. request.files is a dict-like object keyed by the file input's name.
Which form attribute is required to actually send file bytes?
- method='GET'
- accept-charset='utf-8'
- enctype='multipart/form-data'
- type='file'
Answer: enctype='multipart/form-data'. Without enctype='multipart/form-data' the browser sends only the filename as text.
What does secure_filename do?
- sanitizes a user-supplied filename for safe disk use
- encrypts the file
- compresses the upload
- renames the file randomly
Answer: sanitizes a user-supplied filename for safe disk use. It strips path parts like ../ and odd characters, e.g. '../my report.txt' becomes 'my_report.txt'.
Which module provides secure_filename?
- flask.utils
- os.path
- flask.security
- werkzeug.utils
Answer: werkzeug.utils. secure_filename is imported from werkzeug.utils.
How do you save an uploaded FileStorage object to disk?
- f.save(path)
- f.write(path)
- save(f, path)
- f.store(path)
Answer: f.save(path). The FileStorage object exposes .save(path) to write the file to disk.
Which config setting caps the maximum upload size?
- UPLOAD_LIMIT
- MAX_CONTENT_LENGTH
- MAX_FILE_SIZE
- CONTENT_CAP
Answer: MAX_CONTENT_LENGTH. app.config['MAX_CONTENT_LENGTH'] limits the request body in bytes.
What status does Flask return when an upload exceeds MAX_CONTENT_LENGTH?
- 404 Not Found
- 400 Bad Request
- 413 Request Entity Too Large
- 500 Internal Server Error
Answer: 413 Request Entity Too Large. Flask automatically returns 413 before your view runs.
What does FileStorage.filename hold?
- the original name the browser sent
- the absolute server path
- the file's bytes
- the MIME type
Answer: the original name the browser sent. .filename is the original client-supplied name; sanitize it before using it.
Why validate the file extension against an allow-list?
- to make uploads faster
- to compress images
- it is required by HTTP
- to reject unwanted or dangerous file types
Answer: to reject unwanted or dangerous file types. An allow-list of extensions rejects file types you do not want to accept.
How do you safely read an upload that may be missing?
- request.files.get('file')
- file
Answer: request.files.get('file'). request.files.get('file') returns None instead of raising a KeyError when absent.