In this post, you will see how you can design your own signup and login form in Python with user validation and start building your own apps. Before we go to the production approach, we first need to see the beginners model and then part 2 covers the production part.
Table of Contents
Prerequisites:
- Familiarity with Python functions, classes, and little bit about sqlite3.
- Familiarity with Regex.
- Understanding of user input validation.
- Understanding of error validation.
What You Will Learn:
- How to transition from temporary dictionaries to a persistent relational database using
sqlite3. - How to design a database schema suited for complex administrative platforms (including hierarchical roles and statuses).
- How to implement strong password security using Python’s
hashliblibrary. - How to use Regular Expressions (Regex) for strict, reliable email validation.
- How to encapsulate your logic within Python Classes for clean, modular, and reusable code.
- How to handle real-world account management, such as archiving users or toggling editor statuses, rather than just hard-deleting them.
Part 1: Beginner Level For Practice
So, let’s get started.
First of all, we are going to import the regex for email validation and create a database. Here I am using a simple dictionary but you can use Pandas or other database tools based on SQL as per the projects this is sufficient to show you how you can interact with the database.
import re
#create a database for users
users = {}
Next, we are going to write a function to allow users to sign up:
# create a signup form
def signup():
details = {}
# asking for name
name = input("Enter your name")
details["Name"] = name
# asking for age and verifying
try:
age = int(input("Enter your age. You must be at least 13."))
except ValueError:
print("Only numbers are allowed")
else:
if age < 13:
print("You must be at least 13")
else:
details["Age"] = age
# asking for email and verifying
email = input("Enter your email.")
# Create a Regex for Email Addresses
emailRegex = re.compile(r'''(
[a-zA-Z0-9._%+-1]+ #username
@ # @symbol
[a-zA-Z0-9.-]+ #domain name
(\.[a-zA-Z]{2,4}) #dot-something
)''', re.VERBOSE)
e = []
f = emailRegex.search(email)
if f == "None":
print("Enter an email")
else:
try:
e.append(f.group())
if e[0] in users:
print("This email is already taken")
else:
details["Email"] = e[0]
except AttributeError:
print("One email address is needed!")
# enter address
address = input("Enter your address")
details["Address"] = address
# enter username
username = input("Enter your username")
if username in users:
username = input("Enter a different username")
details["Username"] = username
else:
details["Username"] = username
# enter password -- You can also encrypt and decrypt the password using third party libarary
password = input("Enter your password")
details["Password"] = password
# updating our database
users.update({username : details})
print("\n")
print("Your Details Are:")
print(details)
Next, we are going to write a code to allow users to log in:
# to allow users to login
def login():
username = input("Enter your username")
if username not in users:
print("You don't have an account. Please create one")
else:
password = input("Enter your password")
if password != users[username]["Password"]:
print("Password did not match. Try again")
else:
print("You are successfully logged in") # you can input the process you want to start here.
Finally, a code to allow users to delete or remove their data:
# A function to allow users to deleted their accounts
def delete():
username = input("Enter your username")
if username not in users:
username = input("Enter your username")
else:
users.pop(username)
print("Account Deleted")
Part 2: Advanced Level (Close To Production)
Now we are taking a massive leap forward. We will rewrite the basic signup and login script using a much more advanced, robust, and production-ready approach. We will utilize SQLite3 for persistent database management, implement Password Hashing for top-tier security, and structure our code using Object-Oriented Programming (OOP).
Step 1: Establishing a Persistent Database
Temporary variables are great for learning the basics of logic, but professional software requires persistent storage. Python comes with a powerful built-in library called sqlite3, which allows us to interact with an SQL database natively without needing to install complex, external server software.
Let’s begin by creating our database schema. Instead of just saving a username and a password, we will design our system to support different user roles (like ‘editor’ or ‘user’) and dynamic account states.
import sqlite3
def initialize_database():
# Connect to the database file (it will be created if it doesn't exist)
conn = sqlite3.connect('platform_users.db')
cursor = conn.cursor()
# Create the users table with advanced fields
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT DEFAULT 'user',
status TEXT DEFAULT 'active'
)
''')
conn.commit()
conn.close()
print("Database initialized successfully.")
if __name__ == "__main__":
initialize_database()
Explaination:
import sqlite3: We import the built-in SQLite module. Because it’s included in Python’s standard library, no pip installs are necessary.sqlite3.connect('platform_users.db'): This line opens a connection to a database file. If the file doesn’t exist, Python will seamlessly create it right there in your project folder.cursor = conn.cursor(): A cursor is an object that allows us to execute SQL commands one by one. Think of it as your virtual assistant that carries your instructions to the database server.CREATE TABLE IF NOT EXISTS users (...): This is a SQL command. We define our columns meticulously:id: A unique identification number for every user that increments automatically (AUTOINCREMENT).username&email: Defined asUNIQUE NOT NULL, meaning no two users can share the same username or email, and they absolutely cannot be left blank.password_hash: We will store the hash of the password, not the actual text.role: Defaults to ‘user’, but we can selectively upgrade accounts to ‘editor’ or ‘admin’.status: Defaults to ‘active’. This crucial addition allows us to soft-delete or ‘archive’ accounts later without losing data.
conn.commit(): This executes and saves the changes to the database.conn.close(): Always close your connection when you are done to free up system memory and resources.
Step 2: Securing Passwords with Hashing
You must never store plain text passwords. If a malicious actor gains access to your database file, they will instantly have everyone’s passwords. Instead, we use a cryptographic hash function. A hash function takes an input (the password) and returns a fixed-size string of seemingly random characters. It is a one-way street; you cannot easily reverse a hash back into the original password.
We will use Python’s built-in hashlib. For enhanced, production-grade security, we will add a “salt” which is a randomly generated string added to the password before hashing to protect against pre-computed dictionary attacks.
import hashlib
import os
def hash_password(password: str) -> str:
# Generate a random 16-byte salt
salt = os.urandom(16)
# Hash the password combined with the salt using SHA-256
hash_obj = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt,
100000 # Number of iterations
)
# Combine the salt and the hash into a single string for storage
return salt.hex() + ':' + hash_obj.hex()
def verify_password(stored_password: str, provided_password: str) -> bool:
# Split the stored string back into salt and hash
salt_hex, original_hash = stored_password.split(':')
salt = bytes.fromhex(salt_hex)
# Hash the provided password with the extracted salt
hash_obj = hashlib.pbkdf2_hmac(
'sha256',
provided_password.encode('utf-8'),
salt,
100000
)
# Compare the newly generated hash with the original stored hash
return hash_obj.hex() == original_hash
Code Explaination:
os.urandom(16): This generates cryptographically secure random bytes. We use this unique randomness as our ‘salt’ for every individual user.hashlib.pbkdf2_hmac(...): This is a highly robust hashing algorithm. It requires the hashing method (sha256), the password converted to byte format (encode('utf-8')), the salt, and the number of iterations (100,000). The high iteration count intentionally slows down the computational process slightly. To a user logging in, the delay is imperceptible, but it makes brute-force attacks painfully slow for hackers.salt.hex() + ':' + hash_obj.hex(): We need to store both the salt and the hash in the database to verify the password later. We convert both to readable hexadecimal strings and join them with a colon separator.verify_password(...): When a user attempts to log in, we pull this combined string from the database. We split it by the colon to retrieve the unique salt, apply that exact same salt to the newly typed password, and check if the resulting hashes match. If they match, the password is correct!
Step 3: Object-Oriented System Architecture
To ensure our code is clean, scalable, and easy to integrate into larger software systems (like expansive administrative portals), we should wrap our logic inside a Python Class. This allows us to keep the database connection organized and prevents our global namespace from becoming cluttered.
import sqlite3
import re
class AuthManager:
def __init__(self, db_name='platform_users.db'):
# The constructor initializes the database filename for the instance
self.db_name = db_name
def _connect(self):
# A helper method to establish a fresh connection when needed
return sqlite3.connect(self.db_name)
# We will add signup, login, and management methods inside this class next
Explaination:
class AuthManager:: We define a structural blueprint for our authentication system. By treating the authentication system as an object, it can hold its own localized state.def __init__(self, db_name): The constructor method. It triggers automatically the moment we create a newAuthManagerobject. It sets up the database name dynamically.def _connect(self): A utility helper method. The underscore (_) prefix is a Python convention indicating that this method is intended for internal use by the class itself, not to be called externally. It reliably opens a fresh database connection whenever we need to query or insert data.
Step 4: The Advanced Signup Form
Now, let’s implement the core signup process inside our AuthManager class. We need to sequentially take user inputs, strictly validate the user’s email using a Regular Expression (Regex), ensure they meet an administrative age requirement, hash their secure password, and safely insert them into our persistent database.
def signup(self):
print("\n--- Create a New Account ---")
username = input("Choose a username: ").strip()
email = input("Enter your email address: ").strip()
# Strict Email Validation using Regex
email_pattern = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
if not email_pattern.match(email):
print("Error: Invalid email format. Please try again.")
return
# Age Validation with Error Handling
try:
age = int(input("Enter your age (must be 18+ for admin tools): "))
if age < 18:
print("Error: You must be at least 18 years old to register.")
return
except ValueError:
print("Error: Age must be a valid numerical value.")
return
password = input("Create a strong password: ")
if len(password) < 8:
print("Error: Password must be at least 8 characters long.")
return
# Hash the password for secure storage
hashed_pw = hash_password(password)
# Connect to Database safely
conn = self._connect()
cursor = conn.cursor()
try:
# We use placeholders (?) to prevent SQL Injection attacks
cursor.execute('''
INSERT INTO users (username, email, password_hash)
VALUES (?, ?, ?)
''', (username, email, hashed_pw))
conn.commit()
print(f"Success! Account for '{username}' created successfully.")
except sqlite3.IntegrityError:
# Catching the UNIQUE constraint failure gracefully
print("Error: That username or email is already registered in our system.")
finally:
conn.close()
Explaination:
input(...).strip(): The.strip()function actively removes any accidental white spaces (like hitting the spacebar before typing) at the beginning or end of the user’s input, preventing difficult-to-track bugs.re.compile(...): This Regex pattern is a strict, mathematical rulebook for what an email structure should look like. It demands alphabetical characters/numbers, an obligatory@symbol, more characters for the domain name, a literal dot, and a top-level domain (like.comor.net). Ifmatch()fails, we stop the function early using thereturnkeyword.try... except ValueError: When asking for an integer (int()), if the user playfully types the word “twenty” instead of the number “20”, the program would normally crash. Thetry/exceptblock actively catches this error gracefully and prints a helpful message instead.VALUES (?, ?, ?): This is vitally important. Never inject Python variables directly into an SQL string using f-strings (e.g.,f"INSERT INTO users VALUES ({username})"). Doing so opens your software up to SQL Injection, a catastrophic security flaw where users can write malicious code into the input boxes. Using?tells SQLite to safely sanitize the inputs before processing them.sqlite3.IntegrityError: Since we explicitly defined the username and email columns asUNIQUEin Step 1, the database engine itself will forcefully reject duplicate entries. We catch this specific error and inform the user cleanly instead of throwing a massive wall of red error text.
Step 5: The Secure Login System
Logging in involves checking if the provided username exists, fetching their hashed password block from the database, evaluating their account status, and passing the data to our cryptographic verification function.
def login(self):
print("\n--- System Login ---")
username = input("Enter your username: ").strip()
password = input("Enter your password: ")
conn = self._connect()
cursor = conn.cursor()
# Fetch the user's necessary data bundle
cursor.execute("SELECT password_hash, role, status FROM users WHERE username = ?", (username,))
result = cursor.fetchone()
conn.close()
if result is None:
print("Error: Account not found.")
return False
# Unpack the tuple
stored_hash, role, status = result
# Check if the account has been administratively archived
if status == 'archived':
print("Error: This account has been archived. Please contact platform support.")
return False
# Verify the password computationally
if verify_password(stored_hash, password):
print(f"Welcome back, {username}! Role Privileges: [{role.upper()}]")
return True
else:
print("Error: Incorrect password. Access denied.")
return False
Explaination:
cursor.execute("SELECT ..."): We query the database exclusively for the password hash, role, and current status associated with the exact entered username.cursor.fetchone(): This retrieves the very first row that matches our query. Because usernames are entirely unique, there will only ever be a maximum of one match. If the user doesn’t exist, it safely returnsNone.stored_hash, role, status = result: This neat Python trick is called “unpacking.” The database returns a tuple containing three items (e.g.,('the_hash_string', 'user', 'active')), and we assign those three separate values to three named variables instantaneously.- We check the
statusflag immediately. This guarantees that users whose accounts have been archived are physically barred from authenticating, regardless of if they remember their password. - Finally,
verify_password()does the heavy cryptographic lifting.
Step 6: Advanced Account Management (Editor Status & Archiving)
In professional platform environments, you rarely want to permanently delete data using a basic .pop() or DELETE FROM command. Permanent deletion permanently destroys vital historical records. Instead, administrative systems utilize “soft deletes” changing an account’s database status to “archived.”
Furthermore, administrators need the capability to manage platform privileges, such as toggling editor status up and down. Let’s build a dedicated administrative management method to handle this context.
def manage_user(self):
print("\n--- Administrative User Management ---")
target_user = input("Enter the exact username to manage: ").strip()
conn = self._connect()
cursor = conn.cursor()
# Check if target user exists in the system
cursor.execute("SELECT id, role, status FROM users WHERE username = ?", (target_user,))
user = cursor.fetchone()
if not user:
print(f"Error: User '{target_user}' does not exist.")
conn.close()
return
print(f"User Found: {target_user} | Current Role: {user[1]} | Status: {user[2]}")
print("1. Toggle Archive/Active Status")
print("2. Promote to Editor Privileges")
choice = input("Select an action (1-2): ")
if choice == '1':
# Flip the status dynamically
new_status = 'active' if user[2] == 'archived' else 'archived'
cursor.execute("UPDATE users SET status = ? WHERE username = ?", (new_status, target_user))
print(f"Action Completed: User status changed to '{new_status}'.")
elif choice == '2':
if user[1] == 'editor':
print("Notice: User is already an editor.")
else:
cursor.execute("UPDATE users SET role = 'editor' WHERE username = ?", (target_user,))
print("Action Completed: User successfully granted Editor privileges.")
else:
print("Error: Invalid selection.")
conn.commit()
conn.close()
Explaination:
SELECT id, role, status: We fetch the current state of the target user first, allowing us to display it directly and make logical conditional decisions based on their current standings.new_status = 'active' if user[2] == 'archived' else 'archived': This is an inline if-statement known as a ternary operator. It flips the status like a light switch. If they are currently archived, they become active, and vice versa.UPDATE users SET ...: Instead of aggressively executingDELETE FROM, we merely update specific cells in the existing row. This intelligently preserves all of their linked data (which is vital if you are tracking document histories or edits) but fundamentally alters the control flags that dictate what they can do on the platform.
To run this complete system seamlessly, you can set up a simple interactive while loop at the bottom of your Python file to act as your terminal-based application.
def main():
# Ensure the database tables are physically created before starting
initialize_database()
app = AuthManager()
while True:
print("\n=== Main Control Menu ===")
print("1. Sign Up")
print("2. Log In")
print("3. Admin: Manage Users & Editor Status")
print("4. Exit System")
choice = input("Select an option: ")
if choice == '1':
app.signup()
elif choice == '2':
app.login()
elif choice == '3':
# In a deployed app, you would mandate that the currently logged-in user is an admin first!
app.manage_user()
elif choice == '4':
print("Safely exiting system. Goodbye!")
break
else:
print("Invalid choice. Please enter 1, 2, 3, or 4.")
if __name__ == "__main__":
main()
That’s it. Hope this was helpful. Now if you want to move to a new project then see how you can create an e-commerce app in Python here.
Please note: the code above may not organize well on mobile devices so please use a laptop and code along to learn faster.






