Simple Online Voting System With Python (Step-By-Step Guide)

Welcome to a tutorial on how to create a simple Python online voting with Flask and SQLite. Want to create a simple voting (or poll) system in Python? Those Django and database stuff is too confusing? Well, here’s a simple version that I have made for beginners – Read on!

ⓘ I have included a zip file with all the source code at the start of this tutorial, so you don’t have to copy-paste everything… Or if you just want to dive straight in.

 

 

TABLE OF CONTENTS

Download & Notes Python Votes Useful Bits & Links
The End

 

DOWNLOAD & NOTES

Firstly, here is the download link to the example code as promised.

 

QUICK NOTES

  • Create your own project folder, e.g. D:\vote, unzip the code inside this folder.
  • Open the terminal (or command line), navigate to your project folder cd D:\vote.
  • As usual, create a virtual environment if you don’t want to mess up your other projects.
    • virtualenv venv
    • Windows – venv\scripts\activate
    • Mac/Linux – venv/bin/activate
  • Get all the packages – pip install Flask Flask-Session
  • Create the database python s2_create.py
  • Launch! python s4_server.py and access http://localhost.

 

 

EXAMPLE CODE DOWNLOAD

Click here to download all the example source code, I have released it under the MIT license, so feel free to build on top of it or use it in your own project.

 

 

PYTHON VOTING SYSTEM

All right, let us now get into the details of creating a simple voting system with Python Flask and SQLite.

 

STEP 1) DATABASE TABLES

s1_votes.sql
-- (A) QUESTIONS TABLE
CREATE TABLE questions (
  qid INTEGER,
  txt TEXT NOT NULL,
  PRIMARY KEY("qid" AUTOINCREMENT)
);
 
-- (B) OPTIONS TABLE
CREATE TABLE options (
  oid INTEGER,
  qid INTEGER NOT NULL,
  txt TEXT NOT NULL,
  votes INTEGER DEFAULT 0,
  PRIMARY KEY("oid" AUTOINCREMENT)
);
CREATE INDEX idx_qid ON options (qid);

-- (C) DUMMY DATA
INSERT INTO questions (txt) VALUES ("What is your favorite meme animal?");
INSERT INTO options (qid, txt, votes) VALUES
  (1, "Birb", 11), (1, "Doge", 22), (1, "Cate", 33), (1, "Snek", 44);

The first step of every project, deal with the database foundations (and dummy data for testing). Not to worry, there are only 2 simple tables in this project:

Questions
qid Question ID. Primary key and auto-increment.
txt The question itself.
Options
oid Option ID. Primary key and auto-increment.
qid Question ID.
txt The option itself.
votes The number of votes for this option.

 

 

STEP 2) CREATE THE DATABASE

s2_create.py
# (A) LOAD PACKAGES
import sqlite3, os
from sqlite3 import Error

# (B) DATABASE + SQL FILE
DBFILE = "votes.db"
SQLFILE = "s1_votes.sql"

# (C) DELETE OLD DATABASE IF EXIST
if os.path.exists(DBFILE):
  os.remove(DBFILE)

# (D) IMPORT SQL
conn = sqlite3.connect(DBFILE)
with open(SQLFILE) as f:
  conn.executescript(f.read())
conn.commit()
conn.close()
print("Database created!")

Next, we simply create a new database and import the above SQL file.

 

STEP 3) VOTES LIBRARY MODULE

s3_lib.py
# (A) LOAD SQLITE MODULE
import sqlite3
DBFILE = "votes.db"
 
# (B) GET QUESTION + OPTIONS + VOTES
def get(qid):
  # (B1) RETURNS A DICTIONARY (NONE ON ERROR/NOT FOUND)
  res = {
    "q" : "", # QUESTION
    "o" : {}, # OPTIONS, OID : TEXT
    "v" : {} # VOTES, OID : NUMBER OF VOTES
  }
 
  # (B2) GET QUESTION
  conn = sqlite3.connect(DBFILE)
  cursor = conn.cursor()
  cursor.execute("SELECT txt FROM questions WHERE qid=?", (qid,))
  data = cursor.fetchone()
  if data is None:
    return None
 
  # (B3) GET OPTIONS
  res["q"] = data[0]
  cursor.execute("SELECT oid, txt, votes FROM options WHERE qid=?", (qid,))
  data = cursor.fetchall()
  if len(data)==0:
    return None
  for row in data:
    res["o"][row[0]] = row[1]
    res["v"][row[0]] = row[2]
 
  # (B4) RETURN RESULT
  conn.close()
  return res
 
# (C) SAVE VOTE
def save(qid, oid, oldid):
  # (C1) GET CURRENT VOTES COUNT
  conn = sqlite3.connect(DBFILE)
  cursor = conn.cursor()
  cursor.execute("SELECT oid, votes FROM options WHERE qid=?", (qid,))
  data = {}
  for r in cursor.fetchall():
    data[r[0]] = r[1]
 
  # (C2) UPDATE OLD VOTE COUNT
  if oldid is not None:
    count = data[oldid] - 1
    count = count if count>0 else 0
    cursor.execute("UPDATE options SET votes=? WHERE qid=? AND oid=?", (count, qid, oldid,))

  # (C3) UPDATE NEW VOTE COUNT
  count = data[oid] + 1
  cursor.execute("UPDATE options SET votes=? WHERE qid=? AND oid=?", (count, qid, oid,))
 
  # (C4) DONE
  conn.commit()
  conn.close()
  return True

The next piece of the puzzle, create a library module to work with the database. Again, keep calm and look carefully. There are only 2 functions here:

  • get() Self-explanatory. Gets the specified question, along with the options and number of votes.
  • save() Update the vote count. Now, this can be a little hard to grasp without a clear picture of the entire process. But try to follow along:
    • We will store the user’s selected option in session["vote"][qid].
    • So for a new user who has not made any votes before, save(QID, OID, NONE) will simply increment the option’s vote count.
    • If the user decides to change the option, save(QID, OID, OLD OID FROM SESSION) will decrement the old selected option, then increment the newly selected option.

 

 

STEP 4) FLASK SERVER

INITIALIZE

s4_server.py
# (A) INIT
# (A1) LOAD MODULES
from flask import Flask, session, render_template, request
from flask_session import Session
import s3_lib as votes
 
# (A2) FLASK SETTINGS + INIT
HOST_NAME = "localhost"
HOST_PORT = 80
app = Flask(__name__)
# app.debug = True
app.secret_key = "VERY-SECRET-KEY"
app.config["SESSION_TYPE"] = "filesystem"
Session(app)

The first parts of the Flask server script should be easy enough. We are just loading the required modules, and initializing Flask (to also use sessions).

 

ROUTES – VOTES PAGE

s4_server.py
# (B) VOTES PAGE
@app.route("/", methods=["GET", "POST"])
def index():
  # (B1) INIT
  qid = 1 # QUESTION ID, FIXED TO 1 IN THIS EXAMPLE
  if "vote" not in session: # SESSION - KEEP TRACK OF VOTES MADE
    session["vote"] = {}
 
  # (B2) SAVE VOTE ON FORM SUBMIT
  if request.method == "POST":
    oid = int(request.values.get("vote"))
    oldid = None if qid not in session["vote"] else session["vote"][qid]
    votes.save(qid, oid, oldid)
    session["vote"][qid] = oid
 
  # (B3) GET QUESTION FROM DB
  data = votes.get(qid)
  if data is None:
    return "ERROR!"
  data["qid"] = qid
 
  # (B4) RENDER PAGE
  return render_template("s5_vote.html", **data)
 
# (C) START
if __name__ == "__main__":
  app.run(HOST_NAME, HOST_PORT)

Look no further, there is only one page here. Just trace along, and it should explain itself:

  • (B1) As mentioned earlier, we will use session["vote"] to keep the choices that the user has made.
  • (B3 & B4) Get the question, options, and votes from the database – Pass it to be rendered on the HTML page.
  • (B2) When the HTML voting form is submitted, this will update the votes count.

 

 

STEP 5) VOTING HTML PAGE

templates/s5_vote.html
<form method="post" class="poll-docket">
  <!-- (A) QUESTION -->
  <div class="poll-question">{{ q }}</div>
 
  <!-- (B) OPTIONS -->
  {% for oid, txt in o.items() %}
  <label class="poll-option">
    <input type="radio" name="vote" value="{{ oid }}" required
    {% if session["vote"][qid] is defined and oid==session["vote"][qid] %}
     checked
    {% endif %}/>
    <span class="poll-text">{{txt}}</span>
    <span class="poll-votes">({{v[oid]}})</span>
  </label>
  {% endfor %}
 
  <!-- (C) SUBMIT -->
  <input type="submit" class="poll-go" value="Go!"/>
</form>

Just the plain old HTML voting form.

 

USEFUL BITS & LINKS

That’s all for the tutorial, and here is a small section on some extras and links that may be useful to you.

 

WORKING WITH MULTIPLE QUESTIONS

There are nearly endless ways to tweak the above script.

  • Change the script to adapt the qid from GET. For example, http://site.com/vote?qid=123
  • Or maybe even POST.
  • Use the URL route as the qid. For example, http://site.com/vote/456
  • Load via AJAX.
    • Use the “base URL” to serve an empty page first – http://site.com/vote
    • Load the form via AJAX – http://site/vote/789
  • Create your own def showqn(qid) GENERATE HTML TEMPLATE.

Yep, it’s up to you to decide.

 

 

LIMITATIONS

As you can see, this voting system is open to the public and uses the session to track. Anyone can just delete the session cookie and vote multiple times. If you want better “secure votes”, consider this:

  • Create a users system.
  • Only registered users can vote.
  • Create a new votes table, with Question ID (PRIMARY), User ID (PRIMARY), Option ID. This way, a user can only vote for one option for a question once.
  • Then, do security checks before saving the votes.

 

SQLITE & FILE-BASED SESSIONS ARE NOT GOOD

Before the angry “master code ninja” things start to spill acid – SQLite and file-based sessions are easy, but they are not good for professional use:

  • They work on a simple single-server setup but fail horribly on distributed/cloud.
  • Remember that the database/sessions file can only exist on one server. Mirroring these files on all other servers becomes a challenge instead.
  • They are not great when it comes to performance-wise.
  • Also has reliability and security issues.

So yes, PostgreSQL, MySQL, Redis, or MongoDB – At least pick one of these up. SQLite is a good tool for learning, not so much for professional use.

 

LINKS & REFERENCES

 

THE END

Thank you for reading, and we have come to the end. I hope that it has helped you to better understand, and if you want to share anything with this guide, please feel free to comment below. Good luck and happy coding!

Leave a Comment

Your email address will not be published. Required fields are marked *