Persist Data with Local Storage

Learn to persist data with local storage in this mobile app development tutorial. Understand the core concepts, apply hands-on exercises, and compare options to choose the right approach.

Focus: persist data with local storage

Sponsored

You’ve just built a beautiful mobile app with Python — screens, navigation, maybe even a fancy custom widget. But the moment you close the app and reopen it, everything resets. That’s the pain of not persisting data: every launch is a blank slate. In this lesson, you’ll learn how to persist data with local storage, so your app remembers user settings, scores, notes, or any small piece of state — without needing a server. By the end, you’ll be able to save and load data on your device using kivy, json, and platform-specific file paths, and you’ll know which storage option fits your use case.

The problem this lesson solves

Mobile apps are inherently stateless on launch — the operating system reclaims memory when your app closes, and your in-memory Python objects (lists, dicts, instances) vanish. If you’re building a to-do list, a fitness tracker, or a note-taking app, losing all user data every time is a dealbreaker.

Consider these real scenarios:

  • Fitness tracker: user logs their morning run, closes the app, and opens it later — their stats must still be there.
  • Grocery list: user adds items while at the store, then checks them off later — the items must survive app restarts.
  • Settings screen: user toggles dark mode — that preference should persist across sessions.

The core problem is simple: your app needs a way to write data to the device’s filesystem or database and read it back when the app starts again. This lesson focuses on local storage — the simplest, most accessible form of persistence for mobile apps written in Python with frameworks like Kivy or BeeWare.

Core concept / mental model

Think of local storage as a notebook your app keeps on the device. Every time you need to remember something, you write it down on a dedicated page. When you need that information again, you open the notebook and read the page.

The “notebook” here is a file (usually a plain-text file like JSON or CSV) or a lightweight database like SQLite. For simple data, a JSON file is perfect because JSON maps naturally to Python dictionaries and lists.

Here’s the mental model in three steps:

  1. Serialize: turn your Python objects into a string (JSON, for example) that can be saved to a file.
  2. Write: save that string to a location the OS allows you to write to (your app’s data directory).
  3. Deserialize: read the file on app startup and convert the string back into Python objects.

The beauty of local storage is that it’s offline, fast, and private — no network requests, no latency, and your data stays on the device. For most small-scale app needs, it’s the ideal starting point.

How it works step by step

Let’s break down the process of persisting data with local storage into concrete steps.

Step 1: Find the right file path

On Android, iOS, and desktop, you can’t just write to any directory. You need to use a platform-appropriate location. In Python with Kivy, you can use the kivy.utils.platform and app.user_data_dir from a Kivy App instance to get a directory that is guaranteed writable.

  • On Android: /data/data/<package>/files (via user_data_dir)
  • On iOS: <app>/Documents
  • On macOS/Linux/Windows: ~/.local/share/<app> or %APPDATA%

Step 2: Serialize your data to JSON

Use Python’s built-in json module. Convert your data dictionary to a string with json.dumps(). Make sure your data is JSON-serializable (dicts, lists, strings, ints, floats, booleans, None).

Step 3: Write to file

Open the file in write mode ('w') and write the JSON string. Always use with open() to ensure the file is closed properly.

Step 4: Read and deserialize on startup

When your app starts (e.g., in the build() method of a Kivy app), read the file if it exists, parse the JSON with json.loads(), and populate your app’s state.

Step 5: Update data and save periodically

Whenever your data changes, call your save function again. This keeps the file up-to-date. For maximum reliability, save after every critical mutation.

Hands-on walkthrough

Let’s build a simple counter app that persists the count. This example uses Kivy’s App object and user_data_dir.

Example 1: Save and load data in a Kivy app

import json
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.utils import platform
from os.path import join

class CounterApp(App):
    def build(self):
        self.count = self.load_data()
        layout = BoxLayout(orientation='vertical')
        self.label = Label(text=f"Count: {self.count}")
        increment_btn = Button(text='Increment')
        increment_btn.bind(on_press=self.increment)
        reset_btn = Button(text='Reset')
        reset_btn.bind(on_press=self.reset)
        layout.add_widget(self.label)
        layout.add_widget(increment_btn)
        layout.add_widget(reset_btn)
        return layout

    def get_data_file(self):
        return join(self.user_data_dir, 'counter.json')

    def load_data(self):
        try:
            with open(self.get_data_file(), 'r') as f:
                return json.load(f).get('count', 0)
        except (FileNotFoundError, json.JSONDecodeError):
            return 0

    def save_data(self):
        data = {'count': self.count}
        with open(self.get_data_file(), 'w') as f:
            json.dump(data, f)

    def increment(self, instance):
        self.count += 1
        self.label.text = f"Count: {self.count}"
        self.save_data()

    def reset(self, instance):
        self.count = 0
        self.label.text = f"Count: {self.count}"
        self.save_data()

if __name__ == '__main__':
    CounterApp().run()

Expected output: When you run this app, it shows a counter. Every time you tap Increment, the count increases and is saved to counter.json. When you close and reopen the app, the count is restored from the file.

Example 2: Using a generic local_storage helper

For larger apps, encapsulate saving/loading logic in a helper module.

# local_storage.py
import json
from os.path import join, exists

def save_json(app, filename, data):
    with open(join(app.user_data_dir, filename), 'w') as f:
        json.dump(data, f, indent=2)

def load_json(app, filename, default=None):
    path = join(app.user_data_dir, filename)
    if not exists(path):
        return default
    try:
        with open(path, 'r') as f:
            return json.load(f)
    except (json.JSONDecodeError, IOError):
        return default

Then use it in your app:

# main.py
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from local_storage import save_json, load_json

class NotesApp(App):
    def build(self):
        self.notes = load_json(self, 'notes.json', default=[])
        layout = BoxLayout(orientation='vertical')
        self.label = Label(text=f"Notes: {len(self.notes)}")
        add_btn = Button(text='Add note')
        add_btn.bind(on_press=self.add_note)
        layout.add_widget(self.label)
        layout.add_widget(add_btn)
        return layout

    def add_note(self, instance):
        note = f"Note {len(self.notes)+1}"
        self.notes.append(note)
        save_json(self, 'notes.json', self.notes)
        self.label.text = f"Notes: {len(self.notes)}"

if __name__ == '__main__':
    NotesApp().run()

Example 3: Async file I/O with asyncio (advanced)

For large data, consider writing in a separate thread to avoid UI lag.

import asyncio
import aiofiles
from kivy.app import App
from kivy.clock import Clock

class AsyncPersistApp(App):
    async def save_async(self, data):
        path = self.get_data_file()
        async with aiofiles.open(path, 'w') as f:
            await f.write(json.dumps(data))

    def save_in_thread(self, data):
        # Run async save in a thread so UI stays responsive
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        loop.run_until_complete(self.save_async(data))

Pro tip: Always use user_data_dir from Kivy’s App, not a hardcoded path — it guarantees the right platform-specific directory and makes your app portable.

Compare options / when to choose what

Now that you know the basics, let’s compare the main local storage solutions for Python mobile apps:

Method Best for Pros Cons
JSON file Simple key-value data, app settings, small lists Easy, human-readable, built-in json module Not great for huge data, no queries
SQLite Structured data, relational queries, larger datasets ACID compliance, complex queries, mature More boilerplate, requires SQL knowledge
Key-Value stores (e.g., plyer storage, pathlib + file) Very simple preferences, small fragments Minimal code, fast Limited to flat data

For most small to medium apps, I recommend starting with JSON files — they’re easy to debug, and you can inspect the saved data during development. If your app grows to thousands of records and you need to filter or sort, migrate to SQLite.

Rule of thumb: If your data fits in a dictionary or list and you rarely query it, use JSON. If you need relational queries or transactions, use SQLite.

Troubleshooting & edge cases

Here are common pitfalls when you persist data with local storage, and how to fix them.

1. FileNotFoundError on first launch

Your app tries to read a file that doesn’t exist yet.

Fix: Always check if the file exists before opening it, or wrap the read in a try/except.

2. json.JSONDecodeError after manual editing

If the file is corrupted or edited incorrectly, json.loads() will fail.

Fix: Catch this exception and fall back to a default value (e.g., empty list).

3. Data not saving due to wrong file path

Hardcoding paths like /sdcard/data.json may fail on Android due to permissions. Always use user_data_dir.

4. UI freezes when writing large data

Synchronous file I/O can block the main thread. Use threading or async I/O for large payloads.

5. Saving too often (performance)

Writing to disk on every keystroke can be slow. Debounce with a system that saves after a short idle time.

What you learned & what's next

You now understand how to persist data with local storage — from choosing the right file path, serializing to JSON, writing and reading, to troubleshooting common issues. You can apply this to your own mobile apps built with Kivy. You’ve completed a hands-on exercise that saves and loads a counter, and you know when to use JSON vs. SQLite.

Next lesson in this track will cover SQLite integration for more complex data requirements. You’ll build on your local storage knowledge to handle structured data with queries.

Keep this pattern in mind: find the path, serialize, write, read, deserialize. It will serve you in every app you build.

Now go ahead and add persistence to your app — your users will thank you!

Practice recap

Mini-exercise: Extend the counter app to save a list of all previous counts (e.g., [1,2,3...]) and display them on a label. Run the app, increment a few times, close it, and reopen — verify the list persists. Then add a 'Clear History' button that empties the list and saves.

Common mistakes

  • Using a hardcoded file path like /sdcard/data.json — fails on Android due to permissions; always use user_data_dir.
  • Assuming the file exists on first launch — always wrap open() in a try/except FileNotFoundError or check with os.path.exists().
  • Saving every millisecond without debouncing — leads to poor performance and battery drain; save on changes or after a debounce delay.

Variations

  1. Use sqlite3 for structured data that requires queries and transactions.
  2. Leverage plyer's storage API for a cross-platform key-value store.
  3. Use pickle for arbitrary Python objects, but be aware it's not human-readable and has security risks.

Real-world use cases

  • Fitness tracker saving daily step counts and workout logs offline.
  • Note-taking app storing user notes as JSON files on the device.
  • Game saving high scores and level progress to persist player state.

Key takeaways

  • Local storage allows your app to remember data between launches by writing to the device filesystem.
  • Use app.user_data_dir to get the correct writable directory on any platform.
  • Serialize Python objects to JSON with json.dumps() and write to a file; on startup, load and parse with json.loads().
  • Always handle missing or corrupted files gracefully to avoid crashes.
  • Choose JSON for simple data and SQLite for larger, relational datasets.
  • Keep your UI responsive by using async or threaded I/O for large data.

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.