Start from scratch: variables, data types, control flow, functions, and file I/O - everything you need to write your first pipeline scripts.

Python is the scripting language that keeps showing up in art tools. For TAs, it's usually the first language to learn because it's already sitting inside most VFX, games, and animation software.
Maya ships with Python. Houdini ships with Python. Blender, Nuke, Substance Designer, Unreal Engine - all of them expose Python APIs. Learning Python once means you can automate workflows in every tool you'll ever use.
C++ matters for engine-level work, but Python lets you iterate 10x faster on tools and pipeline scripts. MEL is Maya-only. Python is universal - invest in it first, and you'll carry that skill everywhere.
Most DCC applications bundle their own Python interpreter, but for standalone scripting you'll want Python installed on your system.
# In your terminal / command prompt:
# python --version
# Python 3.12.4
# Launch the interactive interpreter:
# python
print("Hello, Technical Artist!")
# Output: Hello, Technical Artist!macOS and most Linux distributions include Python, but it may be an older version. Use python3 --version to check. On macOS, install the latest version via Homebrew: brew install python.
Python 2 reached end-of-life in January 2020. Always use Python 3. Some older Maya versions still defaulted to Python 2, but Maya 2022+ ships with Python 3. Make sure your studio has migrated.
A variable is just a name stuck on a value. Python is dynamically typed, so you don't declare types - assign something and Python figures it out.
# Strings - text data (asset names, file paths, etc.) asset_name = "hero_sword" file_path = "/assets/weapons/hero_sword.fbx" # Integers - whole numbers (frame counts, polygon counts) frame_count = 120 poly_count = 15432 # Floats - decimal numbers (positions, rotations, scale) position_x = 3.14 rotation_y = 90.0 # Booleans - True or False (visibility, toggle states) is_visible = True needs_export = False # None - represents "no value" (unset references) selected_object = None # Check types with type() print(type(asset_name)) # <class 'str'> print(type(frame_count)) # <class 'int'> print(type(position_x)) # <class 'float'> print(type(is_visible)) # <class 'bool'>
String manipulation is something you'll do constantly as a TA - building file paths, parsing asset names, formatting log messages.
# f-strings (Python 3.6+) - the best way to format strings
asset = "hero_sword"
version = 3
path = f"/assets/weapons/{asset}_v{version:03d}.fbx"
print(path) # /assets/weapons/hero_sword_v003.fbx
# Common string methods
filename = "Hero_Sword_Final_v02.fbx"
print(filename.lower()) # hero_sword_final_v02.fbx
print(filename.replace("_", "-")) # Hero-Sword-Final-v02.fbx
print(filename.endswith(".fbx")) # True
print(filename.split("_")) # ['Hero', 'Sword', 'Final', 'v02.fbx']
# Strip whitespace (common when reading files)
raw_line = " some data from a file \n"
clean = raw_line.strip()
print(repr(clean)) # 'some data from a file'# Basic math total_frames = 24 * 10 # 240 half_width = 1920 / 2 # 960.0 (division always returns float) integer_div = 1920 // 2 # 960 (floor division returns int) remainder = 10 % 3 # 1 (modulo - useful for every-nth-frame) # Clamping a value between min and max value = 1.5 clamped = max(0.0, min(1.0, value)) print(clamped) # 1.0 # Converting between types poly_string = "15432" poly_int = int(poly_string) print(poly_int + 100) # 15532
Control flow lets your code make decisions and repeat actions. This is where scripts start becoming genuinely useful.
poly_count = 45000
# Check asset quality tier based on polycount
if poly_count > 100000:
tier = "hero"
print(f"Hero asset: {poly_count:,} polys - full LOD chain needed")
elif poly_count > 30000:
tier = "mid"
print(f"Mid-tier asset: {poly_count:,} polys - 2 LODs recommended")
elif poly_count > 5000:
tier = "background"
print(f"Background asset: {poly_count:,} polys - 1 LOD sufficient")
else:
tier = "simple"
print(f"Simple asset: {poly_count:,} polys - no LODs needed")
# Output: Mid-tier asset: 45,000 polys - 2 LODs recommended# Iterate over a list of assets
assets = ["hero_sword", "shield_01", "potion_health", "armor_plate"]
for asset in assets:
export_path = f"/export/{asset}.fbx"
print(f"Exporting: {export_path}")
# Using enumerate() to get the index too
for i, asset in enumerate(assets):
print(f" [{i + 1}/{len(assets)}] Processing {asset}...")
# Range-based loops (useful for frame ranges)
start_frame = 1
end_frame = 10
for frame in range(start_frame, end_frame + 1):
print(f"Rendering frame {frame:04d}")
# Looping with a step (every 10th frame for a preview)
for frame in range(0, 240, 10):
print(f"Preview frame: {frame}")# Retry a flaky network operation with a max attempt limit
import time
max_retries = 3
attempt = 0
success = False
while attempt < max_retries and not success:
attempt += 1
print(f"Attempt {attempt}/{max_retries}: Uploading asset...")
# Simulate: in real code, this would be your upload logic
if attempt == 3:
success = True
print("Upload complete!")
else:
print("Failed. Retrying in 2 seconds...")
time.sleep(2)Use break to exit a loop early (e.g., stop searching once you've found what you need). Use continue to skip the rest of the current iteration and move to the next item (e.g., skip files that don't match a pattern).
Functions let you organize code into reusable blocks. Every TA tool you build will be composed of functions.
def validate_asset_name(name):
"""Check if an asset name follows our naming convention.
Convention: lowercase, underscores only, no spaces,
must start with a letter.
"""
if not name:
return False, "Name cannot be empty"
if not name[0].isalpha():
return False, "Name must start with a letter"
if " " in name:
return False, "Name cannot contain spaces"
if name != name.lower():
return False, "Name must be lowercase"
if not all(c.isalnum() or c == "_" for c in name):
return False, "Name can only contain letters, numbers, and underscores"
return True, "Valid"
# Using the function
test_names = ["hero_sword", "Hero_Sword", "02_weapon", "my asset", "shield_01"]
for name in test_names:
is_valid, message = validate_asset_name(name)
status = "" if is_valid else ""
print(f" {status} '{name}' - {message}")def build_export_path(asset_name, version=1, extension="fbx", root="/export"):
"""Build a standardized export file path.
Args:
asset_name: Name of the asset (e.g., "hero_sword")
version: Version number (default: 1)
extension: File extension without dot (default: "fbx")
root: Root export directory (default: "/export")
Returns:
Full export path as a string.
"""
version_str = f"v{version:03d}"
return f"{root}/{asset_name}_{version_str}.{extension}"
# All of these work:
print(build_export_path("hero_sword"))
# /export/hero_sword_v001.fbx
print(build_export_path("hero_sword", version=3))
# /export/hero_sword_v003.fbx
print(build_export_path("hero_sword", version=2, extension="obj"))
# /export/hero_sword_v002.obj
print(build_export_path("hero_sword", root="/game/meshes", version=5))
# /game/meshes/hero_sword_v005.fbxAlways add docstrings to your functions. Future-you (and your teammates) will thank you. In DCC tools, docstrings even show up in the script editor's auto-complete!
Reading and writing files is half the job - configs in, logs out, export manifests everywhere in between.
# The 'with' statement ensures the file is properly closed
# even if an error occurs - always use it.
# Read an entire file at once
with open("asset_list.txt", "r") as f:
content = f.read()
print(content)
# Read line by line (memory-efficient for large files)
with open("asset_list.txt", "r") as f:
for line_number, line in enumerate(f, start=1):
asset = line.strip()
if asset: # skip empty lines
print(f" Line {line_number}: {asset}")
# Read all lines into a list
with open("asset_list.txt", "r") as f:
assets = [line.strip() for line in f if line.strip()]
print(f"Found {len(assets)} assets")import datetime
def write_export_log(exported_assets, log_path="export_log.txt"):
"""Write a timestamped export log."""
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(log_path, "w") as f:
f.write(f"Export Log - {timestamp}\n")
f.write("=" * 50 + "\n\n")
for i, asset in enumerate(exported_assets, start=1):
f.write(f"{i:3d}. {asset}\n")
f.write(f"\nTotal: {len(exported_assets)} assets exported.\n")
print(f"Log written to {log_path}")
# Usage
assets = ["hero_sword_v003.fbx", "shield_01_v001.fbx", "potion_health_v002.fbx"]
write_export_log(assets)import datetime
def log_message(message, log_path="pipeline.log"):
"""Append a timestamped message to a log file."""
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(log_path, "a") as f: # "a" = append mode
f.write(f"[{timestamp}] {message}\n")
log_message("Export started for hero_sword")
log_message("Validation passed: 15,432 polys")
log_message("Export complete: /export/hero_sword_v003.fbx")Always specify encoding when working with text files that might contain non-ASCII characters: open("file.txt", "r", encoding="utf-8"). This is especially important when reading files from artists who use non-English characters in asset names or comments.
Let's combine everything into a small but complete script - a tool that scans a directory for texture files and generates a report.
import os
import datetime
def scan_textures(directory, extensions=None):
"""Scan a directory for texture files and return a report.
Args:
directory: Path to scan.
extensions: List of valid extensions (default: common texture formats).
Returns:
A list of dicts with file info.
"""
if extensions is None:
extensions = [".png", ".jpg", ".jpeg", ".tga", ".tiff", ".exr", ".psd"]
textures = []
for filename in os.listdir(directory):
filepath = os.path.join(directory, filename)
# Skip directories
if not os.path.isfile(filepath):
continue
# Check extension
_, ext = os.path.splitext(filename)
if ext.lower() not in extensions:
continue
# Gather info
size_bytes = os.path.getsize(filepath)
size_mb = size_bytes / (1024 * 1024)
textures.append({
"name": filename,
"path": filepath,
"extension": ext.lower(),
"size_mb": round(size_mb, 2),
})
return textures
def generate_report(textures, output_path="texture_report.txt"):
"""Write a texture scan report to a file."""
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(output_path, "w", encoding="utf-8") as f:
f.write(f"Texture Scan Report - {timestamp}\n")
f.write("=" * 60 + "\n\n")
if not textures:
f.write("No textures found.\n")
return
# Sort by size (largest first)
textures.sort(key=lambda t: t["size_mb"], reverse=True)
total_size = 0
for i, tex in enumerate(textures, start=1):
f.write(f"{i:3d}. {tex['name']:<40s} {tex['size_mb']:>8.2f} MB\n")
total_size += tex["size_mb"]
f.write(f"\n{'Total':<45s} {total_size:>8.2f} MB\n")
f.write(f"Files found: {len(textures)}\n")
print(f"Report saved to {output_path}")
# Run the tool
if __name__ == "__main__":
texture_dir = "./textures"
if os.path.isdir(texture_dir):
results = scan_textures(texture_dir)
generate_report(results)
else:
print(f"Directory not found: {texture_dir}")You've now covered the Python basics that every technical artist needs. Here's where to go from here:
Don't just read - type out every code example and run it yourself. Modify the examples: change the asset names, add new validation rules, write to different file formats. The fastest way to learn Python is by writing Python.