📄 simplibs-file
Files you can trust — read, write, copy and inspect, without surprises.
A Python library for everyday work with the content of files, built around one idea:
every operation exists first as a plain, well-behaved function, and is then offered as
a composable Rule (for filters and validation) and as a pipeline Action (for
chained processing). Writes are atomic by default, text is encoded before the disk is
touched, defaults are deterministic (UTF-8, "\n", BOM-tolerant reading), and operations that
fail explain exactly why through structured
simplibs-exception cards instead of leaking
a bare OSError or UnicodeDecodeError.
from simplibs.file import read_json, write_json, copy_file
settings = read_json("config.json") # a BOM-tolerant read, a card on a syntax error
write_json("out/index.json", {"files": 3}, sort_keys=True) # serialized first, then swapped in atomically
copy_file("report.txt", "backup/report.txt") # atomic, keeps permissions and times
simplibs-fileis the sister ofsimplibs-path: that library works with the paths themselves (normalize, check, scan), this one with what is inside the files at those paths. Together they are the building blocks of file-processing pipelines. You can use the plain functions without ever touching rules or actions.
🧭 The Core Philosophy
File handling in Python is easy until it is not: Path.write_text truncates the file before
it encodes, so one unencodable character leaves a half-written file; a crash in the middle of
shutil.copy2 leaves a truncated "backup" that looks real; os.replace between two disks
fails; json.load chokes on a BOM written by a Windows tool; deleting a symlink deletes the
file it points to; and the platform quietly decides your line endings. simplibs-file takes
those traps seriously:
- Functions first. All logic lives in small, independent, reusable functions. Rules and actions are thin layers over them — never a second copy of the logic.
- Safe by default. A write goes into a private draft that replaces the target in one atomic step, so an interruption leaves the old file or the new one, never half of each. A copy and a backup work the same way.
- Encode first, touch the disk later. Every text and JSON writer converts to bytes in memory; a bad character or an unserializable value fails with the target still intact.
- Deterministic defaults. Reads tolerate a BOM (
utf-8-sig), writes are BOM-freeutf-8with"\n"line endings — the same bytes on every machine. Anything else is an explicit choice. - Operations explain themselves. Every failure is a structured, actionable card
(
NotFoundError,StateError,AlreadyExistsError,ConversionError,AccessError, ...) that names the file — or both files — and what the OS reported. - Entry or content, by intent. Copying and backing up work on the content behind a
symlink; moving and deleting work on the directory entry, so
delete_file("link")removes the link and never the file it points to. - Honest about time. A filesystem answer is true now; the docs say where a check is a fail-fast message rather than a guarantee.
📦 Installation
pip install simplibs-file
🚀 Quick Start in 60 Seconds
Level 1: Plain functions
from simplibs.file import read_text, write_text, append_text, read_lines
text = read_text("notes.txt") # the whole file, "\n" line endings
write_text("out/notes.txt", text.upper()) # the folder is created; the write is atomic
append_text("logs/run.log", "finished\n") # creates the file if it is missing
lines = read_lines("notes.txt", keep_ends=True) # every line keeps its original ending
Level 2: Rules as filters and gates
from simplibs.path import collect_files
from simplibs.path.rules import HasSuffix, IsFile
from simplibs.file.rules import ContentIs, IsTextFile, SizeIs
from simplibs.rules.predicates.comparisons import GreaterThan
from simplibs.rules.predicates.strings import StartsWith
extractable = IsFile() & HasSuffix(".py") & IsTextFile() & SizeIs(GreaterThan(0))
collect_files("~/project", include_filter=extractable)
extractable.describe() # -> "existing file & suffix '.py' & text file & size: > 0"
ContentIs(StartsWith("#!")).is_valid("scripts/run.sh") # -> True
Level 3: Actions as pipeline steps
from simplibs.actions import ForEach
from simplibs.path.actions import CollectFiles
from simplibs.path.rules import HasSuffix
from simplibs.file.actions import ReadText, WriteAggregated
from simplibs.file.rules import IsTextFile
merge_notes = (
CollectFiles(include_filter=HasSuffix(".md") & IsTextFile()) # root folder -> list of paths
>> ForEach(ReadText()) # paths -> list of texts
>> WriteAggregated(path="build/all_notes.md") # texts -> the written Path
)
merge_notes("notes") # -> Path('<cwd>/build/all_notes.md')
await merge_notes.acall("notes") # the same, off the event loop
Level 4: Safety helpers
from simplibs.file import SafeFileWriter, backup_file, file_hash, move_file
backup_file("report.txt") # -> report_20261013_142530.txt.bak — never overwritten
move_file("report.txt", "G:/archive/report.txt") # also across devices
file_hash("archive.zip") # -> 'b5bb9d80...' — read in chunks, flat memory
with SafeFileWriter("out/data.json") as handle: # crash-safe, for writes made in several steps
handle.write('{"ok": true}')
🛠️ Architecture & Package Structure
src/simplibs/file/
├── logic/ ◄── The functional core — plain functions, no Rule/Action knowledge
│ ├── read/ ◄── read_bytes, read_text, read_lines, iter_lines, read_json
│ ├── write/ ◄── write_bytes, append_bytes, write_text, append_text, write_lines, write_aggregated, write_json
│ ├── operations/ ◄── copy_file, move_file, rename_file, delete_file, backup_file
│ ├── inspection/ ◄── file_size, file_hash, is_text_file
│ ├── safe_writer/ ◄── SafeFileWriter, IGNORE_DRAFTS
│ └── _common/ ◄── Internal helpers shared by the packages above
├── rules/ ◄── Rule classes over files (domain folder) — IsTextFile, SizeIs, ContentIs
├── actions/ ◄── Action classes over the functions (domain folder)
│ ├── read/ write/ destinations/ operations/ inspection/
└── annotations/ ◄── Shared types — EncodingName, EncodingErrors, NewlineMode, ByteCount, TextCollection, HashAlgorithm, WriterMode
Everything in logic/ and the seven public types is importable straight from simplibs.file.
rules and actions are separate domains, imported through their own folders:
from simplibs.file import read_text, write_json, SafeFileWriter, EncodingName # functions and types
from simplibs.file.rules import IsTextFile, SizeIs, ContentIs # rules
from simplibs.file.actions import ReadText, WriteText # actions
PathLike is not redefined here — it comes from
simplibs.path.
📖 logic/read/ — Everything that reads content
All read a regular file (a symlink is followed), never change anything, and report a missing
file, undecodable bytes or broken JSON as a card. Text readers default to "utf-8-sig", so a
BOM never spoils the result.
| Function | Description / Parameters |
|---|---|
read_bytes |
The raw content — the core every other reader is built on (path). |
read_text |
The whole text, line endings normalized to "\n" (path, encoding="utf-8-sig", errors="strict"). |
read_lines |
A list of lines (path, encoding, errors, keep_ends=False). Only \n, \r\n and \r end a line. |
iter_lines |
The lazy counterpart of read_lines — one line at a time, flat memory (same parameters). |
read_json |
Reads and parses JSON; a syntax error carries the line and column (path, encoding, errors). |
📝 logic/write/ — Everything that writes content
The functions that change the disk. Each returns the normalized target Path, so it chains.
Text is encoded in memory first; the target's folder is created when missing.
| Function | Description / Parameters |
|---|---|
write_bytes |
The first core writer (path, data, overwrite=True, use_draft=True, sync=False). |
append_bytes |
The second core writer — adds to a file, creating it if missing (path, data, use_draft=False, sync=False). |
write_text |
Encodes and writes text (path, text, encoding="utf-8", errors, newline="\n", overwrite, use_draft, sync). |
append_text |
Appends text; a BOM is never written into the middle of a file (path, text, encoding, errors, newline, use_draft=False, sync). |
write_lines |
One item per line (path, lines, ..., trailing_newline=True). |
write_aggregated |
Joins fragments with a separator and writes them as one file (path, parts, separator="\n", ..., trailing_newline=False). |
write_json |
Serializes first, then writes (path, data, indent=2, ensure_ascii=False, sort_keys, allow_nan=False, default, trailing_newline=True, ...). |
📦 logic/operations/ — Work with the file itself
All change the disk and return the normalized path of the result. Copies, backups and cross-device moves are atomic — complete, or not there.
| Function | Description / Parameters |
|---|---|
copy_file |
Atomic copy keeping permissions and times (source, destination, overwrite=False, sync=False). The destination is a file path, never a folder. |
move_file |
Moves or renames — across folders and devices; a symlink is moved as a link (source, destination, overwrite=False). |
rename_file |
The old name of move_file — the very same function. |
delete_file |
Deletes a file or a symlink, never the link's target (path, missing_ok=False, force=False). |
backup_file |
A backup next to the file, never overwriting an older one (path, suffix=".bak", timestamp=True, sync=False). |
🔎 logic/inspection/ — Ask a file about itself
None of them changes anything. file_size and file_hash are strict (a card when there is no
file); is_text_file is a predicate that returns False instead.
| Function | Description / Parameters |
|---|---|
file_size |
The size in bytes, from one stat (path). |
file_hash |
The hex digest of the content, read in chunks (path, algorithm="sha256", chunk_size=1_048_576). |
is_text_file |
Whether the beginning looks like text — decodes and contains no NUL (path, sample_size=8192, encoding="utf-8"). |
🔐 logic/safe_writer/ — Crash-safe writing
The one place that knows how to replace a file safely. Every writer goes through it unless you
opt out with use_draft=False; use it directly when a file is written in several steps.
| Component | Description / Parameters |
|---|---|
SafeFileWriter |
A context manager: writes go into a draft that replaces the target only when the block ends without an exception (target_path, mode="w", encoding, errors, newline, sync). |
IGNORE_DRAFTS |
A scanner ignore_names pattern for leftover drafts — opt-in: DEFAULT_IGNORE_NAMES | IGNORE_DRAFTS. |
📐 rules/ — Rules for files
Every rule is a Rule from simplibs-rules:
composable with &, | and ~, usable as a scanner filter, as a @validate_call annotation
(Annotated[Path, IsTextFile()]) and as a pipeline gate (IsTextFile() >> ReadText()).
They never raise for disk problems — a missing file, a directory or undecodable bytes simply fail
the rule. Questions about the path itself (IsFile, HasSuffix, NameIs, ...) live in
simplibs.path.rules.
| Class | Description / Parameters |
|---|---|
IsTextFile |
A readable file whose beginning looks like text (sample_size=8192, encoding="utf-8"). |
SizeIs |
The size in bytes satisfies another rule (rule) — SizeIs(GreaterThan(0)), SizeIs(InRange(1, 1024)). |
ContentIs |
The beginning of the text satisfies another rule (rule, encoding="utf-8-sig", max_bytes=65_536) — ContentIs(StartsWith("#!")). |
⚙️ actions/ — Pipeline steps
Every function that takes one main value and returns one result is also an Action from
simplibs-actions: configured once, called with
the main input, chainable with >>, & and |, and awaitable through acall.
A writer needs a path and a content, but a pipeline carries one value — so both directions
are offered: WriteText(path=...)("content") (the content flows) and
WriteTextTo(text=...)("path") (the path flows).
| Group | Actions | Main input → result |
|---|---|---|
| read | ReadBytes, ReadText, ReadLines, ReadJson |
path → bytes / str / list[str] / value |
| write | WriteBytes, AppendBytes, WriteText, AppendText, WriteLines, WriteAggregated, WriteJson |
content → written Path |
| destinations | WriteBytesTo, AppendBytesTo, WriteTextTo, AppendTextTo, WriteLinesTo, WriteAggregatedTo, WriteJsonTo |
path → written Path |
| operations | CopyFile, CopyFileFrom, MoveFile, RenameFile, DeleteFile, BackupFile |
path → result Path |
| inspection | FileSize, FileHash |
path → int / str |
Predicates, iter_lines and SafeFileWriter have no action: a Rule is already a valid
pipeline step (it becomes a pass-through gate), and a generator or a context manager does not
fit a one-value-in, one-value-out model.
🏷️ annotations/ — Shared validatable annotations
| Type | Description |
|---|---|
EncodingName |
The name of a text encoding Python can both encode and decode with. Rejects "rot13", "base64", unknown names. |
EncodingErrors |
"strict", "ignore", "replace", "backslashreplace", "surrogateescape", "surrogatepass" — the handlers that work in both directions. |
NewlineMode |
"\n", "\r\n", "\r" or None for the platform's own line ending. |
ByteCount |
A positive number of bytes (a chunk, a sample). Rejects zero, negatives and bool. |
TextCollection |
A collection of texts (lines, fragments). Rejects a bare string, a generator and non-text items. |
HashAlgorithm |
The twelve fixed-length algorithms of hashlib (md5 ... blake2s). |
WriterMode |
The open modes of SafeFileWriter: "w", "wb", "a", "ab". |
Use them in your own signatures so your functions accept exactly what this library does.
🪟 Platform Notes
The library is written for Windows and POSIX alike, and where they differ it says so:
- Line endings: the library writes
"\n"everywhere unless you ask otherwise (newline="\r\n", orNonefor the platform's own) — Python's text mode would silently write\r\non Windows. - BOM: reads tolerate it (
utf-8-sig), writes do not add it. A BOM codec (utf-8-sig,utf-16) writes its BOM once, at the start of a new file — never again on an append. - Locks: on Windows a short lock (an antivirus, the search indexer) is retried when a draft is swapped in or a file is moved; elsewhere a permission problem is real and raised at once.
- Read-only files: Windows cannot delete one —
delete_file(..., force=True)clears the flag first. - Moves between disks (
G:→C:) fall back to an atomic copy followed by removing the source. - Durability:
sync=Trueflushes the file, and — where the OS allows it — the folder. Windows cannot flush a folder; there only the file is synced. - Case: a case-only rename (
a.txt→A.txt) is allowed on Windows.
⚠️ Exceptions
Functions raise structured simplibs.exception
cards rather than bare OSErrors and UnicodeErrors:
| Exception | Raised when |
|---|---|
ValidationError |
An argument has the wrong type or value (a blank path, encoding="rot13") — via validate_call. |
ParamError |
An argument combination is unusable (source and destination are the same file, a malformed suffix). |
NotFoundError |
A required file does not exist. |
StateError |
The filesystem is in the wrong state (a directory where a file is needed, ...). |
AlreadyExistsError |
The destination exists and overwrite=False. |
ConversionError |
Bytes do not decode, a character cannot be encoded, JSON is malformed or cannot be serialized. |
AccessError |
The operating system refused (permissions, a locked file, a full disk). |
OperationError |
An operation ran out of options (no free numbered backup name left). |
Predicates (is_text_file, rules/) never raise for disk problems — they return False.
🔗 Related libraries
simplibs-path— the paths themselves: normalize, check, ensure and scan.simplibs-fileis built on it.simplibs-rules— theRuleengine behindsimplibs.file.rules.simplibs-actions— theActionabstraction and containers behindsimplibs.file.actions.simplibs-validate—@validate_call, which validates every function argument here.simplibs-exception— the structured exception cards.
☯️ About simplibs
All libraries in the simplibs (Simple Libraries) ecosystem share a common engineering philosophy:
- Dyslexia-friendly: We actively minimize cognitive load. Code is atomized into small, self-contained units, files are named directly after the logical task they perform, and explanations describe why something is designed, not just what it is.
- Programmer's Zen: Nothing should be missing, and nothing should be superfluous. We value clean execution paths and robust, understandable code architectures over rushed, messy feature sets.
- Defensive Style: We actively anticipate edge cases and failure modes so that only safe operational paths remain. Our code is built to degrade gracefully rather than crash unexpectedly.
- Minimalism: Find the most direct path to the goal in as few operational steps as possible without taking shortcuts on safety, readability, or completeness.
- Code as Craft: Code should be pleasant to look at, readable at a glance, and evoke structural harmony. We treat software engineering as a precision trade.
🤝 Contributing & Community
This is an open-source project built with love and care. We strongly believe in community collaboration and welcome any feedback, bug reports, or feature ideas!
- Want to contribute? Feel free to open an Issue or submit a Pull Request.
- Want to get in touch? If you'd like to discuss the project further, collaborate, or just say hello, feel free to open a GitHub Issue or start a Discussion.
📝 License
This library is released under the MIT License. Build great things!
Metadata
Release files for simplibs-file 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| simplibs_file-0.1.0.tar.gz | 85.4 kB | Without provenance | Details for simplibs_file-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| simplibs_file-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for simplibs_file-0.1.0-py3-none-any.whl |
Total release size: 216.9 kB
Release files / simplibs_file-0.1.0.tar.gz
| Download URL | simplibs_file-0.1.0.tar.gz |
|---|---|
| Size | 85.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
598fd72681c3e1a6d04b3d63553165082de9d1e25b7ab7b4ccb934f7572ff97c
|
|
BLAKE2b-256 checksum How to use checksums |
cb900255c7649926b6b5f92a66bfa21403a46aa25867dcfca5913285bbb96086
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|
Release files / simplibs_file-0.1.0-py3-none-any.whl
| Download URL | simplibs_file-0.1.0-py3-none-any.whl |
|---|---|
| Size | 131.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a7ebb6d06cb79b531d9e92f15378d23eefc920373cae479c1cfd45ce0afff715
|
|
BLAKE2b-256 checksum How to use checksums |
6720be8778ba5ba7e387e972c0d8ff94199fb5aae9d287f67b1afb9c44e245dd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|
Security for simplibs-file 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.