This project extends our Tkinter real-time data tracker by adding persistent storage with SQLite.
The project has two parts.
The worker thread performs only the external data request. Results are sent through a Queue so database writes, StringVar updates and Matplotlib drawing remain on the Tkinter main thread.
External data
|
v
Worker thread
|
v
Queue
|
v
Tkinter main thread
|
+--> SQLite
|
+--> Matplotlib
|
+--> Labels
SQLite
|
v
Combobox
|
v
Treeview viewer
Show Table of Contents
The first application combines Tkinter, threading, SQLite and Matplotlib.
The program lets the user choose one of several Indian stocks or market indices from a Combobox.
After tracking starts, Python periodically:
import queue
import sqlite3
import threading
import tkinter as tk
from tkinter import ttk
import yfinance as yf
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
The main roles are:
The database file is stored in the same directory as the Python program.
db_path=os.path.join(
os.path.dirname(
os.path.abspath(__file__)
),
'stock_data.db'
)
The table keeps the same columns as the earlier version of this project so an existing database remains compatible.
def create_database():
with sqlite3.connect(db_path) as conn:
conn.execute(
'''CREATE TABLE IF NOT EXISTS stock_prices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
stock_name TEXT NOT NULL,
price REAL NOT NULL,
timestamp TEXT NOT NULL
)'''
)
conn.execute(
'''CREATE INDEX IF NOT EXISTS
idx_stock_prices_name_id
ON stock_prices(stock_name,id)'''
)
The index helps when the viewer requests records for one stock ordered by their ID.
The fetching function is independent of Tkinter and SQLite.
def fetch_stock_price(ticker):
stock=yf.Ticker(ticker)
data=stock.history(
period='1d',
interval='1m'
)
if data.empty:
raise ValueError(
'No intraday data was returned.'
)
close_values=data['Close'].dropna()
if close_values.empty:
raise ValueError(
'No price data is available.'
)
price=float(
close_values.iloc[-1]
)
timestamp=close_values.index[-1]
return price,timestamp
The worker performs only the potentially slow external request.
def fetch_worker(
tracker_id,
ticker,
stock_name
):
try:
price,timestamp=fetch_stock_price(
ticker
)
result_queue.put(
(
'ok',
tracker_id,
stock_name,
price,
timestamp
)
)
except Exception as e:
result_queue.put(
(
'error',
tracker_id,
stock_name,
str(e),
None
)
)
tracker_id identifies the current tracking session. If the user
changes stocks before an older request finishes, that older result can be
ignored.
canvas.draw(),
StringVar.set() or other Tkinter methods directly.
When the main Tkinter thread receives a successful worker result, the record is inserted using parameters.
def save_to_database(
stock_name,
price,
timestamp
):
with sqlite3.connect(db_path) as conn:
conn.execute(
'''INSERT INTO stock_prices
(stock_name,price,timestamp)
VALUES (?,?,?)''',
(
stock_name,
price,
timestamp
)
)
The timestamp stored is the timestamp associated with the returned market data rather than simply the computer's current clock time.
Matplotlib is embedded in Tkinter using
FigureCanvasTkAgg.
The graph is updated only from the Tkinter main thread.
def plot_graph(stock_name):
ax.clear()
ax.plot(
times,
prices,
marker='o'
)
ax.set_title(
f'Latest Price Data - {stock_name}'
)
ax.set_xlabel(
'Time'
)
ax.set_ylabel(
'Price / Index Value'
)
fig.autofmt_xdate()
canvas.draw_idle()
The example keeps only the latest 60 plotted points so the in-memory graph does not grow indefinitely.
Starting another stock does not call thread.join() from the GUI
thread.
Instead, the application increments the tracking-session ID and starts a new request.
tracker_id+=1
If an old worker returns later, its ID no longer matches the active session and the result is ignored.
This keeps the GUI responsive even if the previous network request takes time to finish.
The older program performed both:
plot_graph(stock_name)
canvas.draw()
inside the worker thread.
The revised design keeps the worker focused on the external request and passes the result back to Tkinter.
Worker:
fetch data
Main thread:
save data
update StringVar
update graph
This also makes the project easier to extend because fetching, persistence and presentation have separate responsibilities.
The second application opens the same
stock_data.db file and displays saved observations using a
Treeview.
The user chooses a stock from a read-only Combobox.
The viewer then loads the latest stored records for that stock.
There is no need to hardcode the stock list in the viewer. We can ask SQLite which stock names are actually present.
def load_stock_names():
with sqlite3.connect(db_path) as conn:
rows=conn.execute(
'''SELECT DISTINCT stock_name
FROM stock_prices
ORDER BY stock_name'''
).fetchall()
stock_names=[
row[0]
for row in rows
]
stock_dropdown['values']=stock_names
If no rows exist, the interface displays a message rather than trying to
access stock_names[0].
The viewer uses explicit columns:
SELECT
id,
stock_name,
price,
timestamp
FROM stock_prices
WHERE stock_name=?
ORDER BY id DESC
LIMIT ?
The selected stock and limit are passed as parameters.
rows=conn.execute(
query,
(
selected_stock,
MAX_ROWS
)
).fetchall()
The database ID is used as the Treeview iid.
tree.insert(
'',
tk.END,
iid=str(row[0]),
values=(
row[1],
f'{row[2]:.2f}',
row[3]
)
)
The example loads a maximum of 500 recent records so an old database with thousands of observations does not immediately insert every row into the GUI.
The worker does not draw Matplotlib graphs or modify Tkinter widgets.
The Start and close callbacks do not wait synchronously for a sleeping worker thread.
The application avoids repeatedly requesting one-minute data every five seconds.
The original columns remain:
id
stock_name
price
timestamp
This means an existing stock_data.db created by the older
tutorial can still be opened by the revised viewer.
Only the latest 500 records are loaded by default.
The selected stock is never concatenated into the SQL statement.
The unique SQLite id becomes the Treeview iid,
even though it does not have to be displayed as a visible column.
This page combines fetching, storage, graphing and database viewing. The next tutorial focuses more closely on the visualization side of the project.
Real-Time Stock GraphSQLite gives the application persistent local storage so observations can be viewed again after the tracking application is closed.
Tkinter and the embedded Matplotlib canvas belong to the GUI thread. The worker fetches data and sends its result through a Queue.
Short-lived local connections keep connection ownership clear and avoid sharing one SQLite connection across unrelated operations or threads.
No. It represents the latest available value returned by the source and may be delayed depending on market status and data availability.
Limiting the in-memory list prevents the graph from growing indefinitely during a long tracking session. All stored observations can still remain in SQLite.
Yes. The revised code keeps the original stock_prices table columns for compatibility.
A Treeview does not need to load an unlimited database history at once. Limiting the first display keeps the interface responsive. Pagination or date filters can be added later.
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.