Read and Write Device Files Safely

Master safe file I/O on mobile devices with this hands-on tutorial. Learn to handle permissions, avoid common pitfalls, and implement best practices for reading and writing device files in Python.

Focus: read and write device files safely

Sponsored

You've built a beautiful app, and it works perfectly in the simulator. But the moment you install it on a real device, the first file write crashes the app — or worse, silently corrupts data. Mobile file I/O is deceptively simple, and unsafe dependencies, wrong paths, and permission gaps aren't warning shots; they're the top reasons apps get rejected or deleted. This lesson gives you a battle-tested, secure pattern to read and write device files safely, so your app survives the harsh reality of sandboxed storage and flaky file systems.

The problem this lesson solves

Every mobile app needs to store something: a settings file, a cached image, or a user document. But unlike a server where you control the environment, a mobile device throws constraints at you:

  • Sandboxed storage — your app can only see its own tiny slice of the file system.
  • User permissions — some directories require explicit user consent, and the OS can revoke it at any moment.
  • App lifecycle — the OS can kill your app mid-write, leaving a corrupted file behind.
  • Resource scarcity — mobile devices have limited battery, memory, and disk space.

If you ignore these, you get crash logs like PermissionError: [Errno 13] Permission denied or, worse, data loss that erodes user trust. This lesson gives you the defensive toolkit to handle all of it with confidence.

Core concept / mental model

Think of a mobile device file system as a locked building with several rooms:

  • Your app's private room (app_data): only you have the key; no permission needed.
  • Shared lobby (shared_storage): everyone can enter, but you must ask the door guard (permission prompt) — and they can padlock the door anytime.
  • Cache closet (cache): a tiny, high-turnover storage where the janitor (OS) cleans up when space is tight.

Your job is to always know which room you're in and to write a file as if the power could go out at any second. That means never writing directly to the final path, never assuming a directory exists, and always closing resources — even when your code throws an exception.

Here's the mental model distilled into two principles:

  1. Path resolution first — never hardcode /sdcard or C:\Users\...; ask the system for the correct base directory.
  2. Atomic writes — write to a temporary file, then rename it. If the app crashes, the original file stays intact.

How it works step by step

Let's walk through the canonical safe file I/O flow — this is the same pattern you'll use in Kivy, BeeWare, or even a Python backend that needs mobile-like safety.

Step 1: Resolve the correct base directory

I never use absolute paths. Instead, I use a platform-aware helper to get the home directory or app-data directory. In a Kivy app, App.get_running_app().user_data_dir is the blessed location. For a generic Python script deploying to a device, Path.home() is a good start.

Step 2: Ensure the directory exists

os.makedirs(parents=True, exist_ok=True) is your friend. Without it, a single missing parent directory causes a FileNotFoundError.

Step 3: Acquire permission (if needed)

For public storage (Android's shared storage), you must request the WRITE_EXTERNAL_STORAGE permission. On modern Android (API 30+), even that's restricted — prefer app-specific directories. If you must use public storage, use the platform's permission API to request it at runtime, not just at install.

Step 4: Write atomically

Never write to the final filename directly. Write to a temp file in the same directory, flush it, and then os.replace (which is atomic on Unix and Windows). This way, even a crash mid-write leaves the original unchanged.

Step 5: Always close your resources

Use with open(...) or try/finally to guarantee the file handle is released. Leaked handles are a silent killer on Android — they fill up the file descriptor table and eventually crash the app.

Hands-on walkthrough

Let's put it all together with a complete Python script you can run on a desktop first, then adapt to your mobile frame. This script safely writes a settings JSON and reads it back.

Example 1: Basic safe write and read

import json
import os
import tempfile
from pathlib import Path

def safe_write_json(path: Path, data: dict) -> None:
    """Atomically write a JSON object to `path`."""
    path.parent.mkdir(parents=True, exist_ok=True)
    # Use a temp file in the SAME directory to guarantee atomic rename
    fd, tmp_path = tempfile.mkstemp(dir=path.parent, suffix='.tmp')
    try:
        with os.fdopen(fd, 'w', encoding='utf-8') as f:
            json.dump(data, f, indent=2)
            f.flush()
            os.fsync(f.fileno())  # Force data to disk, not just OS cache
        os.replace(tmp_path, path)
    except Exception:
        # Clean up the temp file on failure
        if os.path.exists(tmp_path):
            os.unlink(tmp_path)
        raise

def safe_read_json(path: Path, default: dict) -> dict:
    """Read JSON or return default on any error (never crash)."""
    if not path.exists():
        return default
    try:
        with open(path, 'r', encoding='utf-8') as f:
            return json.load(f)
    except (json.JSONDecodeError, OSError):
        # Corrupted file — don't fail, just reconcile
        return default

# Example usage
settings_path = Path.home() / '.myapp' / 'settings.json'
my_settings = {'theme': 'dark', 'volume': 0.8}
safe_write_json(settings_path, my_settings)
print(safe_read_json(settings_path, {}))
# Output: {'theme': 'dark', 'volume': 0.8}

Example 2: Mobile-specific directory handling (Kivy)

In a Kivy app, you get a safe app-specific directory automatically. Here's how to use it:

from kivy.app import App
from kivy.uix.label import Label
import json
from pathlib import Path

class MyApp(App):
    def build(self):
        return Label(text='Hello')

    def on_start(self):
        # This directory is created and managed by the OS — NO permissions needed
        data_dir = Path(self.user_data_dir)
        stats_file = data_dir / 'session_stats.json'
        # Use the same safe_write_json/read_json functions from Example 1
        if not stats_file.exists():
            safe_write_json(stats_file, {'launches': 0})
        stats = safe_read_json(stats_file, {'launches': 0})
        stats['launches'] += 1
        safe_write_json(stats_file, stats)

Example 3: Handling binary data (images, blobs)

Don't assume JSON; many files are bytes. Write the same atomic pattern, but with 'wb' and 'rb' modes:

def safe_write_bytes(path: Path, data: bytes) -> None:
    """Atomically write binary data."""
    path.parent.mkdir(parents=True, exist_ok=True)
    fd, tmp_path = tempfile.mkstemp(dir=path.parent, suffix='.tmp')
    try:
        with os.fdopen(fd, 'wb') as f:
            f.write(data)
            f.flush()
            os.fsync(f.fileno())
        os.replace(tmp_path, path)
    except Exception:
        if os.path.exists(tmp_path):
            os.unlink(tmp_path)
        raise

Compare options / when to choose what

Storage Type Use Case Permissions Safety Risk
App-specific directory (user_data_dir) Settings, user-created documents, databases None Low — sandboxed
Cache directory Temporary images, thumbnails None Low — but OS can wipe it anytime
Shared storage (public) Exporting files to user's file manager Runtime permission required High — permission can be revoked
Cloud (optional) Syncing across devices Network permission Medium — data leaves device

Rule of thumb: Always use the app-specific directory unless you have a compelling reason to share. It keeps your app secure and avoids permission headaches.

Troubleshooting & edge cases

  • PermissionError: [Errno 13] Permission denied — You used an absolute path outside your app's sandbox, or the user revoked a runtime permission. Fix: resolve the correct directory via the app's API; never hardcode /sdcard. On Android 11+, request permission before acting.
  • FileNotFoundError when writing — You forgot mkdir(parents=True). Fix: always call path.parent.mkdir(...) first.
  • File corrupted after a crash — You wrote directly to the final path. Fix: switch to the atomic pattern with os.replace.
  • PermissionError on os.replace — Temp file was created in a different filesystem (e.g., system temp). Fix: always place the temp file in the same directory as the target.
  • JSON decode error on read — You interrupted a write. Fix: implement safe_read_json to return a default on parse failure.

What you learned & what's next

You've learned the core idea behind reading and writing device files safely: resolve the right path, ensure the directory exists, request permissions only when necessary, write atomically, and always close resources. You can now apply this to a practical exercise, like persisting user preferences or an image cache in your mobile app.

In the next lesson, you'll build on this foundation to handle file synchronization and backup, where you'll manage multiple files, versioning, and conflict resolution. Your safe I/O skills will be the bedrock for that.

Pro tip: Always keep a default in your read functions. A safe app is one that never crashes on a corrupted file.

Practice recap

Open your mobile app project and replace every raw open() call with safe safe_write_json / safe_read_json functions from this lesson. Then, on your device, force-stop the app mid-crash and verify your data survives. If you're on Android, test what happens when you revoke storage permission in Settings — the app should degrade gracefully.

Common mistakes

  • Hardcoding absolute paths like /sdcard/ or C:\data instead of using the app's sandboxed directory.
  • Writing directly to the final path instead of using a temp file + os.replace — a crash tears a hole in the file.
  • Forgetting to create parent directories with mkdir(parents=True, exist_ok=True) — results in FileNotFoundError.
  • Ignoring file descriptor leaks: forgetting with open(...) or not flushing before replace.
  • Reading JSON without a default/fallback — a single corrupted byte crashes the app.

Variations

  1. Use pathlib.Path consistently instead of os.path for cleaner, cross-platform code.
  2. For high-write scenarios, use a database (SQLite) instead of flat files — it's ACID and easier to keep safe.
  3. On Android, prefer the android.storage APIs to access app-specific directories rather than guessing paths.

Real-world use cases

  • Persisting app settings (theme, volume, user preferences) in a lightweight JSON file.
  • Caching remote images or video thumbnails in the app's cache directory for offline viewing.
  • Exporting a user-generated document (e.g., report) to shared storage so the user can find it in their file manager.

Key takeaways

  • Always resolve the correct base directory from the app's API — never hardcode absolute paths.
  • Create directories with mkdir(parents=True, exist_ok=True) before writing.
  • Write atomically: temp file → flush → os.replace to prevent corruption.
  • Use with open(...) or try/finally to close files even on exceptions.
  • Design read functions to return defaults on failure to keep the app crash-free.
  • Prefer app-specific storage over shared storage to avoid permission issues.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.