pola-rs/polars · error · IsADirectoryError

expected a file path; {path!r} is a directory

Error message

expected a file path; {path!r} is a directory

What it means

IsADirectoryError from normalize_filepath (py-polars/src/polars/_utils/various.py:255-265). Most polars IO entry points (read_csv, scan_parquet, read_ipc, etc.) pass the user's path through normalize_filepath with check_not_directory=True; if the expanded path exists on disk and is a directory, polars refuses it up front instead of letting the OS produce a confusing read failure. Multi-file directory reads are intentionally not implicit: use an explicit glob.

Source

Thrown at py-polars/src/polars/_utils/various.py:264

def arrlen(obj: Any) -> int | None:
    """Return length of (non-string/dict) sequence; returns None for non-sequences."""
    try:
        return None if isinstance(obj, (str, bytes, dict)) else len(obj)
    except TypeError:
        return None


def normalize_filepath(path: str | Path, *, check_not_directory: bool = True) -> str:
    """Create a string path, expanding the home directory if present."""
    # don't use pathlib here as it modifies slashes (s3:// -> s3:/)
    path = os.path.expanduser(path)  # noqa: PTH111
    if (
        check_not_directory
        and os.path.exists(path)  # noqa: PTH110
        and os.path.isdir(path)  # noqa: PTH112
    ):
        msg = f"expected a file path; {path!r} is a directory"
        raise IsADirectoryError(msg)
    return path


def parse_version(version: Sequence[str | int]) -> tuple[int, ...]:
    """Simple version parser; split into a tuple of ints for comparison."""
    if isinstance(version, str):
        version = version.split(".")
    return tuple(int(re.sub(r"\D", "", str(v))) for v in version)


def ordered_unique(values: Sequence[Any]) -> list[Any]:
    """Return unique list of sequence values, maintaining their order of appearance."""
    seen: set[Any] = set()
    add_ = seen.add
    return [v for v in values if not (v in seen or add_(v))]


def deduplicate_names(names: Iterable[str]) -> list[str]:

View on GitHub (pinned to df599052da)

Solutions

  1. Pass a file path or an explicit glob pattern, e.g. pl.scan_csv('data/*.csv')
  2. If you want one file per call, iterate: [pl.read_csv(p) for p in sorted(Path('data').glob('*.csv'))]
  3. Fix the upstream path variable that accidentally holds a directory
  4. For single-file inputs, validate with Path(p).is_file() before calling the reader

Example fix

# before
lf = pl.scan_parquet('partitioned_table/')  # IsADirectoryError

# after
lf = pl.scan_parquet('partitioned_table/**/*.parquet')
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def as_readable_path(p: str | Path) -> str:
    p = Path(p).expanduser()
    if p.is_dir():
        raise IsADirectoryError(f'{p} is a directory; pass a file or glob')
    return str(p)

Try / catch

try:
    df = pl.read_parquet(path)
except IsADirectoryError:
    df = pl.read_parquet(str(Path(path) / '**' / '*.parquet'))

Prevention

When it happens

Trigger: pl.scan_csv('data/') or pl.read_parquet('my_folder') where the argument is a directory; also passing a Path object that points at a directory. Expansion of ~ happens first, so '~/data' that is a directory also raises.

Common situations: Assuming read_csv on a directory reads all contained CSVs (pandas-like glob assumption); configurable path inputs that resolve to a folder in one environment and a file in another; download scripts writing to a directory then passing it back as a file path.

Related errors


AI-assisted analysis of pola-rs/polars@df599052da (2026-08-16). Data as JSON: /api/errors/56d7558a7d31c320. Report an issue: GitHub.