Getting started: from JavaScript's dynamic arrays to scripting for AI
For most web developers, JavaScript is the undisputed default — an everything-is-asynchronous model built on the Event Loop, running directly in the browser. But the moment you step into Artificial Intelligence and Machine Learning, Python is the industry standard and there is no arguing with it. That switch is easy to dread: new syntax, and a completely unfamiliar way of managing tools.
Don't worry. This lesson builds you a bridge from the JavaScript concepts you already know to their Python equivalents. You'll find Python surprisingly easy to pick up once the two languages are placed side by side — statement by statement, package manager by package manager, and especially in how each one handles memory. Get this lesson right and you'll be writing your first scripts with confidence, in a clean environment ready for training AI.
python3 --version
If it prints something like
Python 3.11.6, you're set — skip the rest of this box. If it
says command not found, you don't have Python yet: macOS:
brew install python3 · Windows: download the
installer from python.org and make sure you tick "Add Python to PATH" on the first screen
(skipping that box is the single most common reason the terminal can't find Python after installing) ·
Linux: sudo apt install python3 python3-venv. One small thing that trips people up: on macOS and Linux the command is
python3, not
python. On Windows it's usually python. This lesson writes
python3; drop the 3 if you're on Windows.
1. The runtime: Python interpreter vs Node.js
The biggest thing Python and JavaScript have in common is that both are interpreted languages with dynamic typing. You write code and run it directly — no separate compile step like C++ or Java — and a variable can hold a number on one line and a string on the next. Your mental model for running code carries over from Node.js almost unchanged.
There is really only one difference worth caring about right now: Python is noticeably slower than JavaScript at raw arithmetic inside loops. In exchange, it glues extremely well to libraries written in C and C++ — and as the next section shows, that is precisely why the entire AI field picked Python despite the slowness.
The internals below are not needed to complete this lesson. They're here for the curious, and you can skip them entirely without affecting anything later.
Python's default interpreter is called CPython (written in C — hence the C). It also turns your code into bytecode — you'll see
.pyc files appear in a
__pycache__ folder after the first run — but it does not take the further step of
compiling to optimised machine code. The bytecode is executed one instruction at a time on a virtual
machine. That is the root of Python being slower in tight loops.
So why does Python dominate AI and deep learning, which are enormously CPU- and GPU-hungry? Because the foundational tensor libraries — NumPy and PyTorch — aren't really written in Python at all. Their cores are written in C++ and CUDA (NVIDIA's language for running computation on graphics cards). When PyTorch multiplies two matrices, it pushes the whole operation down into multithreaded C++ machine code running outside CPython's control. So that computation bypasses the GIL completely.
Put differently: in AI, Python doesn't do the computing. It gives orders. The heavy lifting is done by C++ and CUDA, where the GIL cannot reach. That's how a "slow" language became the language of AI.
A tensor is just a multi-dimensional array of numbers. That's it. Nothing mystical. You have already used them in JavaScript without calling them that:
5— a single number. A 0-dimensional tensor (also called a scalar).[1, 2, 3]— a flat array. A 1-dimensional tensor (a vector).-
[[1, 2], [3, 4]]— an array of arrays. A 2-dimensional tensor (a matrix). - 3-dimensional, 4-dimensional… still tensors. The number of dimensions is called the rank.
Two examples to show the boundary. A gradebook of 100 students × 5 subjects is a 2-dimensional tensor: all numbers, uniform shape, so matrix arithmetic works on it. But a list of 100 records like
{ name: "An", age: 20, note: "excellent" } is not a
tensor: it contains text, and the entries aren't uniform. To use it in AI you must turn it into numbers
first — and that is exactly the job of the embeddings lessons (Lesson 8). Why does AI only speak in tensors? Because graphics cards are built to do one thing extremely fast: add and multiply large, regular blocks of numbers, thousands of cells at once. Everything in deep learning — images, sentences, audio — is reduced to tensors so it can use that power.
-
npm vs pip:
npm installbecomespip install. The PyPI registry (Python Package Index) plays the role of npmjs.org. -
package.json vs requirements.txt: instead of listing dependencies in a JSON file,
Python uses a plain text file,
requirements.txt, holding library names with versions (for examplenumpy==1.24.3). -
node_modules vs venv (virtual environment): this is the big one. Node.js
automatically creates a local
node_modulesfolder in your project to isolate dependencies. Python, by default, installs libraries into a global system folder. To avoid version clashes between projects — dependency hell — you must create an isolated virtual environment with thevenvtool.
venv really does is create a lightweight copy of the Python interpreter and adjust your
shell's PATH and VIRTUAL_ENV variables so the interpreter looks for libraries
in the project's local venv folder before falling back to the global sys.path.
To create a virtual environment inside your project folder — the equivalent of having your own isolated dependency folder — run these commands in a terminal:
# The word "venv" appears TWICE here, in two different roles:
# -m venv = run Python's standard module named `venv`
# venv = name of the DIRECTORY to create (.venv or env work just as well)
python3 -m venv venv
# Activate on macOS / Linux
source venv/bin/activate
# Activate on Windows (PowerShell)
# venv\Scripts\Activate.ps1
# Sign it worked: your shell prompt gains a (venv) prefix
# (venv) you@machine myproject %
# A stronger check — the path must point inside your project's venv:
which python # macOS/Linux -> .../myproject/venv/bin/python
# where python # Windows
# From now on, pip installs into venv/ instead of the system
pip install numpy pandas
# When you are done
deactivate
source venv/bin/activate again.
This is not a bug — activation only edits the environment of that one shell session. The
symptom when you forget: ModuleNotFoundError: No module named 'numpy' even though you
clearly remember installing numpy yesterday. Two: never commit the venv folder to git. It's a copy of the interpreter plus every library, hundreds of megabytes, and it only works on your exact machine. Add
venv/ to
.gitignore — its role is identical to node_modules/. Three: how you actually share libraries with someone else. Since venv isn't committed, you export the library list to a file instead:
pip freeze > requirements.txt. Whoever
clones the project creates their own venv and runs pip install -r requirements.txt. This is
the package.json + npm install pair you already know, under different names.
| Concept | JavaScript (Node.js) | Python (CPython) |
|---|---|---|
| Package management | npm (Node Package Manager) | pip (Package Installer for Python) |
| Dependency isolation | node_modules (created locally by default) | venv / conda (you must activate it yourself) |
| Dependency manifest | package.json | requirements.txt |
| Execution model | JIT compilation (V8 engine) | Interpreter / bytecode VM |
2. Data types & reference semantics, seen through a JS lens
The minimum syntax you need to read any code in this series
Before we talk about data types, we need to clear three syntax differences that will stop you at the very first line of Python you read. These aren't style tips — without them the code blocks below are genuinely hard to follow.
One: Python has no curly braces. Blocks are defined by indentation.
Wherever you'd write { in JavaScript, Python writes a colon : and indents;
wherever you'd close with }, Python simply dedents. This isn't a formatting convention — it
is syntax: wrong indentation is a program error, not an aesthetic one.
// ===== JavaScript =====
function greet(name) {
if (name) {
console.log(`Hello ${name}`);
} else {
console.log("Hello there");
}
}
const scores = [8, 9, 10];
for (const s of scores) {
console.log(s);
}
# ===== Python — same logic, not a single curly brace =====
def greet(name): # JS `{` becomes a colon `:`
if name:
print(f"Hello {name}") # console.log becomes print
else:
print("Hello there")
# no `}` — dedenting is what ends the block
scores = [8, 9, 10]
for s in scores:
print(s)
Two: print() replaces console.log(), and f-strings replace template
literals.
In JavaScript you interpolate with backticks and ${name}. Python does exactly the same but
puts an f immediately before the quote and uses bare braces: f"Hello {name}".
Forgetting the f is a very common mistake — Python then prints the literal string
Hello {name} instead of the value, and it raises no error at all. You'll see
f-strings used in the hands-on project in section 4.
Three: the four collection types, and what they actually look like. The comparison below
goes into what they are internally, but first look at them on the page — especially how you access a
dict, which is where Python differs most from JavaScript:
# LIST — like a JS Array. Mutable, ordered.
scores = [8, 9, 10]
scores.append(7) # JS: scores.push(7)
print(len(scores)) # JS: scores.length -> 4
# DICT — like a JS Object/Map.
# But you INDEX IT WITH BRACKETS, never with a dot!
student = {"name": "An", "age": 20}
print(student["name"]) # 'An' ← correct
# print(student.name) # AttributeError! Python dicts have no dot syntax
student["grade"] = "12A" # adding a new key
# TUPLE — like a list but FROZEN. Written with parentheses.
point = (10.5, 20.3)
# point[0] = 99 # TypeError: cannot reassign
x, y = point # unpacking into two variables — very common in Python
# SET — unique items, no order. Like a JS Set.
tags = {"ai", "python", "ai"}
print(tags) # {'ai', 'python'} — the duplicate is dropped
{} is a dict, not a setdict and set, so the empty case is ambiguous — and
Python resolves it in favour of dict. x = {} creates an empty dict, not an
empty set. For an empty set you must write x = set(). Check quickly with
type(x) — that function returns a variable's type and will be your most
used diagnostic tool during your first week of Python.
Mutable and immutable — the foundation of every silent data bug
Python's data syntax is very close to JavaScript's, but Python draws a hard line between types that can be changed (mutable) and types that cannot (immutable), right at the core of its design, to optimise memory. This distinction isn't academic: it is the direct cause of the two pitfalls at the end of this section, and both belong to the category "raises no error, just returns wrong results".
Now let's look more deeply at the four types you just saw, this time at how they work internally:
- List (an ordered sequence): the equivalent of a JS Array. Underneath, a list is a dynamic array that grows itself (over-allocating headroom) when it fills up, so it doesn't have to reallocate memory on every append.
- Dict (dictionary): a key-value hash table, equivalent to a JS Object/Map. Since Python 3.6 a dict is memory-optimised and preserves insertion order, by keeping a compact 1-dimensional index array alongside the array that stores the actual values.
-
Tuple: a fixed, read-only list (an immutable list), written with parentheses
(val1, val2). Because it cannot change, a tuple is faster to read than a list and — unlike a list — can be used as adictkey, since its hash value is stable.
.map() or .filter() always
allocates a brand-new intermediate array in RAM. In Python, alongside list comprehensions
[x**2 for x in data] (which do create a new list),
you have the generator expression — just swap the square brackets for parentheses:
(x**2 for x in data). A generator uses lazy evaluation: it computes the
next value only when asked for it (yield). That lets you process millions of records
without spending a single extra byte of RAM, which is ideal for the data-loading pipelines of AI.
b = a and then modifying b, which silently modifies the original
a as well. Here is the comparison in code:
# ❌ WRONG: plain assignment shares the reference, so the original changes too
list_a = [1, 2, 3]
list_b = list_a # copies only the memory address, not the contents
list_b.append(4)
print(list_a) # [1, 2, 3, 4] — the original was modified behind your back!
# ✅ RIGHT: make a shallow copy with .copy()
list_c = [1, 2, 3]
list_d = list_c.copy()
list_d.append(4)
print(list_c) # [1, 2, 3] — the original is safe
For nested structures — a dictionary holding lists or other dictionaries — you need the standard library's
copy module and its deepcopy() function, which clones every level independently:
import copy
# A nested structure: the dict holds a list inside it
original = {"name": "AI Model", "layers": [128, 64, 32]}
# deepcopy clones every level, not just the outer dict
cloned = copy.deepcopy(original)
cloned["layers"].append(16)
print(original["layers"]) # [128, 64, 32] — untouched
print(cloned["layers"]) # [128, 64, 32, 16] — changed independently
function f(arr = []) {...} is created fresh on every call. In Python, the
default expression (def f(arr=[]):) is evaluated exactly once, at the
moment the function is defined — not on each call. Since a list is mutable, every later call reuses that
same list, accumulating data from previous calls.
# ❌ WRONG: the default list is "remembered" across separate calls
def add_log(entry, history=[]):
history.append(entry)
return history
print(add_log("request-1")) # ['request-1'] — looks fine
print(add_log("request-2")) # ['request-1', 'request-2'] — WRONG! expected ['request-2']
# history=[] is created ONCE when def runs; every later call reuses that same list
# ✅ RIGHT: default to None, then build a fresh list inside the body
def add_log_fixed(entry, history=None):
if history is None:
history = [] # a brand-new list on every call
history.append(entry)
return history
print(add_log_fixed("request-1")) # ['request-1']
print(add_log_fixed("request-2")) # ['request-2'] — correct, independent of the last call
This bug is especially dangerous in AI data code because it is "silent" — it throws no exception, it just quietly accumulates junk across calls (imagine a batch-preprocessing function using a default list to "cache" intermediate results), skewing your training results in a way that is very hard to trace back to its source.
3. Asynchronous programming: asyncio vs the Event Loop
In JavaScript, asynchronous programming is the default. The browser or Node.js runs an Event Loop continuously in the background, handling click events, network requests and non-blocking file reads through the Call Stack, the Microtask Queue (promises) and the Macrotask Queue.
Python, by contrast, is fundamentally synchronous and blocking. Run a heavy computation and the thread
stops dead. To go asynchronous, Python has the standard library asyncio, built on something
called a coroutine.
What is a coroutine? Briefly: a function that can pause partway through to let
something else run, then resume exactly where it left off. In Python you create one with
async def, and the pause points are wherever the keyword await appears. You have
already used this exact concept in JavaScript — an async function is a coroutine too, just
under a different name.
In JavaScript, calling
fetchData() starts running it immediately; you get
back a Promise representing work that is already in progress. In Python, calling
fetch_data() runs not a single line of the function body. It returns a
coroutine object — like a ticket that says "this work needs doing", sitting idle until somebody
actually takes it and does it. Who takes it? Either
await (if you're inside another coroutine), or
asyncio.run() at the outermost level. That is the concrete meaning of "Python's Event Loop does not run in the background by itself": JavaScript always has an Event Loop standing by to accept work, whereas in Python you have to start
one yourself with asyncio.run(). How to recognise this mistake: Python prints the warning
RuntimeWarning: coroutine 'fetch_data' was never awaited and your function silently doesn't
run. Seeing that line means you forgot an await or an asyncio.run().
import asyncio
async def fetch_data():
print("Fetching data...")
return 42
# ❌ Called the JavaScript way: NOTHING is printed
result = fetch_data()
print(result) # <coroutine object fetch_data at 0x104f...> ← just the "ticket"
# ✅ The ticket has to be handed to someone who runs it
result = asyncio.run(fetch_data())
print(result) # prints "Fetching data..." then 42
time.sleep(5) — inside a Python async function, the entire asyncio Event Loop
freezes solid. Every other async task running alongside it stops. To avoid this you must either use
async-compatible libraries (such as aiohttp for networking) or move the heavy work onto a
separate thread. The concrete fix (Python 3.9 and later): use
asyncio.to_thread() to push the blocking
function onto a background thread, freeing the Event Loop to keep serving other async tasks while it
waits:
import asyncio
import time
def cpu_heavy_task():
time.sleep(3) # stands in for heavy CPU work, or an old library with no async API
return "Heavy work done"
async def ticker():
for i in range(5):
print(f"Tick {i} — the Event Loop is still running...")
await asyncio.sleep(0.5)
async def main():
# ❌ Calling cpu_heavy_task() directly here would freeze ticker() for 3 seconds.
# ✅ Hand it to a separate thread; the Event Loop stays free to run ticker():
result_task = asyncio.create_task(asyncio.to_thread(cpu_heavy_task))
ticker_task = asyncio.create_task(ticker())
result = await result_task
print(result)
await ticker_task
asyncio.run(main())
# Output: "Tick 0..4" keeps printing every 0.5s while cpu_heavy_task() runs for 3s
Now let's compare the shape of async/await code between JavaScript and Python:
// JavaScript version (Node.js)
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
async function fetchMetadata() {
console.log("Download starting...");
await delay(1000); // non-blocking wait
console.log("Download finished.");
return { status: 200 };
}
// Just call it — JS already has an Event Loop running in the background
fetchMetadata();
# Python version (asyncio)
import asyncio
async def fetch_metadata():
print("Download starting...")
await asyncio.sleep(1) # non-blocking wait, the equivalent of delay()
print("Download finished.")
return {"status": 200}
# You must start the Event Loop yourself
result = asyncio.run(fetch_metadata())
4. Hands-on project: a raw data collection and normalisation tool
To turn theory into practice, we'll build a complete Python script that runs inside a
venv virtual environment. The script does three things in order: it
generates a raw_feedback.json file holding a few raw pieces of user feedback
(you don't need to prepare any input file — the script creates its own so this lesson runs immediately),
it reads that file back and cleans each line of text, and it writes the result out to
cleaned_feedback.csv.
The "cleaning" here is deliberately simple — trim surrounding whitespace and lowercase everything — but the three-step shape of read → transform → write is the skeleton of every data pipeline you'll meet through the rest of the series, right up to the millions of documents feeding the RAG system in Lesson 14.
import json
import csv
import os
def clean_text(text):
"""
Basic text cleaning: trim surrounding whitespace and lowercase.
"""
# Real-world data always contains empty cells and wrong types. A text helper
# that does not guard against them breaks the pipeline at record 10,000.
if not isinstance(text, str):
return ""
return text.strip().lower()
def run_cleaning_pipeline(input_path, output_path):
print("--- Starting the data cleaning pipeline ---")
if not os.path.exists(input_path):
print(f"Error: file {input_path} does not exist.")
return
# `with` closes the file on the way out, even if an error is raised inside
# the block. It is Python's version of the try/finally you write by hand in
# JavaScript. And encoding="utf-8" is not optional: drop it and accented
# text turns into garbage characters on Windows.
with open(input_path, "r", encoding="utf-8") as file:
raw_data = json.load(file)
cleaned_records = []
for index, record in enumerate(raw_data):
# .get(key, fallback) returns the fallback instead of raising when the
# key is missing — the safe way to read data that came from outside.
user_id = record.get("id", index)
raw_comment = record.get("comment", "")
cleaned_records.append({
"user_id": user_id,
"cleaned_comment": clean_text(raw_comment)
})
# Write the cleaned rows out as CSV, ready for the NLP lessons later on.
with open(output_path, "w", newline="", encoding="utf-8") as csv_file:
fieldnames = ["user_id", "cleaned_comment"]
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(cleaned_records)
print(f"Done. Wrote {len(cleaned_records)} cleaned records to {output_path}")
# Everything below runs only when this file is executed directly,
# not when it is imported from another module.
if __name__ == "__main__":
# The sample input is generated here so the lesson runs with no setup.
# These comments stay in Vietnamese on purpose: they are the DATA being
# cleaned, and they demonstrate why encoding="utf-8" matters above.
mock_data = [
{"id": 101, "comment": " Mô hình AI chạy RẤT NHANH! "},
{"id": 102, "comment": "Tôi Cần hỗ Trợ kỹ thuật gấp... "},
{"id": 103, "comment": " Tuyệt VỜI, 10 điểm. "}
]
with open("raw_feedback.json", "w", encoding="utf-8") as f:
json.dump(mock_data, f, ensure_ascii=False, indent=2)
run_cleaning_pipeline("raw_feedback.json", "cleaned_feedback.csv")
That block is long, but only three spots deserve a careful read. First,
if not isinstance(text, str): return "" inside clean_text — real data always has
empty cells and wrong types, and a text function that doesn't defend itself will break the whole pipeline
at record 10,000. Second, with open(...) as f: the with keyword closes the file
when the block exits, even if an error is raised partway through — it's Python's version of the
try/finally you write by hand in JavaScript. Third, encoding="utf-8" on every
file open: leave it out and accented text turns into garbage characters on Windows, which is the most
needlessly time-consuming bug there is when handling non-English data.
To run this project on your own machine, follow exactly these steps:
-
Open a terminal in the folder containing the code file and create the virtual environment:
python3 -m venv venv. -
Activate it:
source venv/bin/activate(macOS/Linux) orvenv\\Scripts\\activate(Windows). - Run the script:
python data_cleaner.py. -
Check the project folder. You'll see two new files:
raw_feedback.json— the raw data the script generated itself in the first step — andcleaned_feedback.csvholding the comments with whitespace trimmed and text lowercased. Opening both side by side is the fastest way to see exactly whatclean_textdid.
Lesson summary & bridge to what's next
- Achieved: command of basic Python syntax and how its data types differ from JavaScript's.
-
Achieved: the ability to set up a
venvvirtual environment and manage libraries withpipin place ofnpm/node_modules.
Bridge to the next lesson: with a working Python mindset in place, the next lesson steps into the mathematical foundation of artificial intelligence: vector operations, matrices and derivatives, programmed visually in pure Python.
Download the hands-on code for this lesson
The Python file data_cleaner.py — the data processing and normalisation script used in this
lesson (run python data_cleaner.py, nothing else to install):
Comments