You are building a FastAPI endpoint that accepts a list of file paths. You write os.path.exists(files) where files is a list — and Python crashes with TypeError: expected str, bytes or os.PathLike object, not list. It trips up nearly every developer working with file handling in FastAPI, Django, or any Python framework that accepts lists. Here is exactly why it happens and how to fix it instantly.
In this tutorial, you will learn to:
- Understand exactly what causes
TypeError: expected str, bytes or os.PathLike object, not list - Reproduce it with clear, minimal examples
- Fix it using list comprehension,
map(), andall()patterns - Prevent it from happening again with type hints and Pydantic validation
Page Contents
What Does the Error Mean?
The error TypeError: expected str, bytes or os.PathLike object, not list means you passed a Python list where a string, bytes, or path-like object was expected. Every function in Python’s os module — os.path.exists(), os.path.join(), os.path.isfile(), os.path.isdir() — operates on a single path at a time. When you pass a list, Python does not know how to convert it to a path string, so it raises this error.
This error is common in FastAPI endpoints where a request body contains a JSON array of file paths, and in Django views that process batches of uploaded files. Understanding Python’s exception handling (as covered in Python Exception Handling) helps you write better error messages when these issues surface in production.
The Problem — Minimal Reproduction
Here is the most common way this error appears in FastAPI:
from fastapi import FastAPI, Body
import os.path
app = FastAPI()
@app.post("/check-files")
async def check_files(files: list[str] = Body(...)):
# This looks logical — check if all files exist
results = os.path.exists(files) # ❌ TypeError raised here
return {"results": results}
When you call this endpoint with {"files": ["/tmp/report.pdf", "/tmp/data.csv"]}, Python raises:
Traceback (most recent call last):
File "...", line 8, in check_files
results = os.path.exists(files)
TypeError: expected str, bytes or os.PathLike object, not list
The same error occurs with os.path.join() when you accidentally pass a list as one of the arguments:
import os.path
# Trying to join multiple base paths from a list
base_paths = ["/home/user", "/opt/data"]
full_path = os.path.join(base_paths, "reports") # ❌ TypeError
# Or joining a list of segments instead of individual strings
segments = ["home", "user", "documents"]
result = os.path.join(segments) # ❌ TypeError
Cause — Why Does This Happen?
Python’s os.path functions are designed to work with individual path strings, not sequences of paths. When you call os.path.exists(path), Python internally calls os.fspath(path) to convert the argument to a file system path string. os.fspath() accepts:
- str — a plain string path like
"/tmp/file.txt" - bytes — a bytes object like
b"/tmp/file.txt" - os.PathLike — an object implementing
__fspath__(), likepathlib.Path
A Python list satisfies none of these. Python cannot guess whether you want the first element, all elements joined, or something else. So it raises TypeError instead.
Here is what os.fspath() does when it receives a list:
import os
files = ["/tmp/a.csv", "/tmp/b.csv"]
try:
os.fspath(files)
except TypeError as e:
print(f"Error: {e}")
Solution 1 — List Comprehension (Most Common)
To check which files in a list exist, use a list comprehension that calls os.path.exists() on each element individually:
import os
files = ["/tmp/report.pdf", "/tmp/data.csv", "/nonexistent.txt"]
# ✅ Check each file individually with list comprehension
results = [os.path.exists(f) for f in files]
print(results) # [True, True, False]
This is the most idiomatic and readable solution in Python. It scales to any list size and is immediately clear to anyone reading your code. If you are new to list comprehensions, they are covered in detail in the Python strings tutorial alongside other fundamental Python patterns.
Solution 2 — Using map()
Python’s built-in map() function applies a function to every item in an iterable without needing a comprehension:
import os
files = ["/tmp/report.pdf", "/tmp/data.csv", "/nonexistent.txt"]
# ✅ map() returns an iterator — wrap with list() to get a list
results = list(map(os.path.exists, files))
print(results) # [True, True, False]
map(os.path.exists, files) is concise and functional in style. It is slightly less readable than a comprehension for beginners, but performs identically and uses less memory on very large lists since it is lazy (iterator-based).
Solution 3 — Check If ALL Files Exist
If you need to verify that every file in the list exists (for example, to fail fast before processing), use all() with a generator expression:
import os
files = ["/tmp/report.pdf", "/tmp/data.csv"]
# ✅ True only if ALL files exist
all_exist = all(os.path.exists(f) for f in files)
print(all_exist) # True or False
The generator expression (os.path.exists(f) for f in files) is memory-efficient because it stops evaluating as soon as it finds a False value — useful if you have thousands of files to check. Generators and iterators are explained in depth in the Python Iterators, Generators, and yield Explained guide.
Solution 4 — Fix os.path.join() with Multiple Paths
If you need to join multiple base directories with a sub-path, use os.path.join() inside a loop or comprehension:
import os
# ❌ Wrong — os.path.join() does not accept a list as first argument
base_paths = ["/home/user", "/opt/data"]
# full_path = os.path.join(base_paths, "reports") # TypeError
# ✅ Correct — join each base path individually
base_paths = ["/home/user", "/opt/data"]
full_paths = [os.path.join(base, "reports") for base in base_paths]
print(full_paths) # ['/home/user/reports', '/opt/data/reports']
If you are joining a list of path segments into a single path, use os.path.join() with the unpacking operator *:
import os
# ✅ Unpack list into join() arguments
segments = ["home", "user", "documents", "file.txt"]
full_path = os.path.join(*segments)
print(full_path) # home/user/documents/file.txt
Solution 5 — Using pathlib Instead
Python’s pathlib module (pathlib.Path) offers a cleaner, object-oriented API for path operations and works naturally in loops and comprehensions:
from pathlib import Path
files = ["/tmp/report.pdf", "/tmp/data.csv", "/nonexistent.txt"]
# ✅ Check existence with pathlib
results = [Path(f).exists() for f in files]
print(results) # [True, True, False]
# ✅ Check if ALL files exist
all_exist = all(Path(f).exists() for f in files)
print(all_exist) # False
# ✅ Join paths with pathlib
reports = Path("/home/user/reports")
csv_files = list(reports.glob("*.csv"))
print(csv_files)
pathlib.Path is the modern Python approach (available since Python 3.4) and is recommended in the official Python documentation over the older os.path functions. When building Django or FastAPI applications, pathlib pairs well with Django 6’s new features for file handling.
FastAPI-Specific Fix
Here is the corrected FastAPI endpoint that properly handles a list of file paths:
from fastapi import FastAPI, Body
from pathlib import Path
app = FastAPI()
@app.post("/check-files")
async def check_files(files: list[str] = Body(...)):
# ✅ Use list comprehension with pathlib
results = [Path(f).exists() for f in files]
# ✅ Or check if ALL files exist
all_exist = all(Path(f).exists() for f in files)
return {
"files": files,
"results": results,
"all_exist": all_exist
}
FastAPI automatically validates the list[str] type hint using Pydantic under the hood. If the request body does not contain an array of strings, FastAPI returns a clear validation error — not the confusing TypeError from os.path.
Prevention — Using Type Hints and Validation
The best way to prevent this error is to validate your data at the boundary of your application using Pydantic models. This gives you clear error messages instead of TypeError deep inside your logic:
from fastapi import FastAPI
from pydantic import BaseModel, field_validator
from pathlib import Path
app = FastAPI()
class FileCheckRequest(BaseModel):
files: list[str]
@field_validator("files")
@classmethod
def validate_paths(cls, v: list[str]) -> list[str]:
for i, path in enumerate(v):
if not isinstance(path, str) or not path.strip():
raise ValueError(f"Path at index {i} must be a non-empty string")
return v
@app.post("/check-files")
async def check_files(request: FileCheckRequest):
results = [Path(f).exists() for f in request.files]
return {"results": results}
Pydantic v2 (used by FastAPI) validates the type before your endpoint function runs. If someone sends a list containing non-string values like [1, 2, None], Pydantic raises a clear ValidationError identifying exactly which field failed and why — far more helpful than a bare TypeError. See the Pydantic validators documentation for the full reference.
Common Traps to Avoid
- Using
os.path.exists(list)directly — always iterate over the list - Assuming
os.path.join(*list)works the same as unpacking — the*unpacking operator is required:os.path.join(*segments) - Mixing
strandbytesin a list —os.path.exists()can handle either, but not both mixed without conversion - Passing
Nonetoos.path.exists()— raisesTypeError: expected str, bytes or os.PathLike object, not NoneType
Related Errors You Might Encounter
TypeError: expected str, bytes or os.PathLike object, not NoneType— when a path isNoneinstead of a stringTypeError: not supported between instances of 'str' and 'int'— when mixing types inos.path.join()TypeError: cannot pickle 'Path' object— when passingpathlib.Pathto multiprocessing or distributed task queues
Summary
The error TypeError: expected str, bytes or os.PathLike object, not list occurs when you pass a Python list to a function expecting a single path string. The fix is always to iterate over the list explicitly — using a list comprehension, map(), or all(). For modern Python code, pathlib.Path offers a cleaner API that handles this naturally.
For FastAPI and Django applications, always validate incoming data with Pydantic or Django forms at the application boundary. This converts confusing TypeError messages into clear, actionable validation errors for your API consumers.
Frequently Asked Questions
Can I use os.path.exists() on a list of files directly?
No. os.path.exists() accepts a single path argument only. You must iterate over the list using [os.path.exists(f) for f in files] or list(map(os.path.exists, files)). Python does not automatically expand a list into multiple function arguments for path functions.
Does pathlib.Path.exists() work the same way?
Yes — pathlib.Path.exists() has the same constraint: it takes one path at a time. You still need to iterate: [Path(f).exists() for f in files]. The difference is that pathlib has a cleaner API and is the recommended modern approach.
Why does os.path.join() raise this error?
os.path.join() expects individual string arguments: os.path.join("home", "user", "file.txt"). If you pass a list as a single argument — os.path.join(["home", "user"]) — it raises TypeError. Use os.path.join(*path_list) to unpack the list into arguments, or [os.path.join(base, sub) for base in bases] to join each element individually.

