A phone number like +12025550148 looks numeric enough for a CSV importer to treat it as an integer. That works until you need the + back. The same problem turns 07700900123 into 7700900123, and the database no longer knows how many zeros were at the start.
Here's a small local import that keeps those values intact. It uses Python's standard library and an in-memory SQLite database, so you can run it without installing a package or setting up a database server.
Reproduce the problem with two rows
Run this in a fresh SQLite session.
CREATE TABLE example (phone NUMERIC); INSERT INTO example VALUES ('+12025550148'), ('07700900123'); SELECT phone, typeof(phone) FROM example ORDER BY rowid; | Stored value | Storage type |
|---|---|
12025550148 | integer |
7700900123 | integer |
The quotes in the INSERT make the inputs strings, but SQLite's NUMERIC affinity still converts them to integers. To preserve the original representation, keep the values as strings in Python and use a TEXT column in SQLite.
Import the CSV as text
Save this example as members.csv. It uses fictional data and two columns from a group CSV exported by WAExport, a Chrome extension we build for WhatsApp Web. The extension can export member numbers from selected groups and include the source group for each row. whatsapp group number extractor
Phone Number,Group Name +12025550148,"Launch, US" 07700900123,QA Sandbox +12025550148,Beta Testers Save the following as inspect_members.py in the same folder. The importer accepts extra columns and reads UTF-8 CSV files with or without a byte order mark.
import csv import sqlite3 from contextlib import closing with closing(sqlite3.connect(":memory:")) as db: db.execute(""" CREATE TABLE group_members ( phone TEXT NOT NULL, source_group TEXT NOT NULL ) """) with open("members.csv", encoding="utf-8-sig", newline="") as source: reader = csv.DictReader(source) required = {"Phone Number", "Group Name"} if not required.issubset(reader.fieldnames or []): raise SystemExit("CSV must contain Phone Number and Group Name.") with db: for row in reader: phone = row["Phone Number"] group = row["Group Name"] if not phone or not group: raise SystemExit(f"Missing value at CSV line {reader.line_num}.") db.execute( "INSERT INTO group_members (phone, source_group) VALUES (?, ?)", (phone, group), ) for phone, group, storage_type in db.execute(""" SELECT phone, source_group, typeof(phone) FROM group_members ORDER BY rowid """): print(f"{phone} | {group} | {storage_type}") Run python inspect_members.py. You should see three rows.
+12025550148 | Launch, US | text 07700900123 | QA Sandbox | text +12025550148 | Beta Testers | text csv.DictReader keeps the phone field as a string and handles the comma inside the quoted group name. SQL placeholders pass each field as a value, and typeof(phone) confirms the storage type. The repeated number still appears twice because it belongs to two source groups.
The import preserves the phone field exactly as it appears in the CSV. If you later need to match numbers from different sources, keep this raw column and derive a separate normalized value using the country information available to you. Once an earlier spreadsheet or import has stripped a leading zero, this script can't recover it from the altered file.
Disclosure: ChatGPT drafted this article and its code example. The example was run with the fictional CSV shown above.