Usage Guide
This guide covers practical usage of the two main classes — FrameSet and
FileSequence — as well as the filesystem discovery functions and how to
extend the library through subclassing.
FrameSet
A FrameSet holds an ordered, deduplicated set of frame numbers parsed from a
range string. It behaves like an immutable sequence and supports set-algebra operators.
Range syntax
from fileseq import FrameSet
FrameSet('1-10') # 1, 2, 3, …, 10
FrameSet('1-10x2') # step: 1, 3, 5, 7, 9
FrameSet('1-10y2') # fill (inverse step): 2, 4, 6, 8, 10
FrameSet('1-10:3') # stagger: 1-10x3, 1-10x2, 1-10
FrameSet('-5-5') # negative frames: -5, -4, …, 5
FrameSet('1-5,10-20') # comma-separated ranges
Constructing from iterables or a single frame
FrameSet([1, 2, 3, 4, 5])
FrameSet(42) # single frame
FrameSet(FrameSet('1-5')) # copy constructor
Subframe support
Pass Decimal values or a subframe range string to work with fractional frame numbers:
from fileseq import FrameSet
FrameSet('1-3x0.5') # 1.0, 1.5, 2.0, 2.5, 3.0
FrameSet([0, 0.25, 0.5, 1]) # from an iterable of floats
Iteration and indexing
fs = FrameSet('1-10x2')
list(fs) # [1, 3, 5, 7, 9]
fs[0] # 1
fs[-1] # 9
len(fs) # 5
5 in fs # True
Set operations
a = FrameSet('1-5')
b = FrameSet('3-7')
a | b # union: 1-7
a & b # intersection: 3-5
a - b # difference: 1-2
a ^ b # symmetric difference: 1-2, 6-7
String representation
str(FrameSet('1-10x2')) # '1-9x2'
FrameSet('1-5,10-20').frameRange # '1-5,10-20'
FileSequence
A FileSequence represents a path pattern paired with a frame range and a
padding token.
Pattern syntax
/path/to/name.{framerange}{padding}.ext
Supported padding tokens:
Token |
Width |
|---|---|
|
4 digits ( |
|
4 digits |
|
1 digit |
|
printf-style (any width) |
|
Houdini-style (any width) |
|
UDIM tile identifier (no padding) |
Basic construction
from fileseq import FileSequence
seq = FileSequence('/render/beauty.1-100#.exr')
print(seq.basename()) # 'beauty'
print(seq.extension()) # '.exr'
print(seq.padding()) # '#'
print(seq.frameRange()) # '1-100'
Accessing individual frames
seq = FileSequence('/render/beauty.1-10#.exr')
seq.frame(1) # '/render/beauty.0001.exr'
seq[0] # first frame path (same as seq.frame(seq.start()))
list(seq) # all 10 file paths as strings
Alternative padding styles
FileSequence('/render/beauty.1-10@@@.exr') # 3-digit padding
FileSequence('/render/beauty.1-10%04d.exr') # printf-style
FileSequence('/render/beauty.1-10$F4.exr') # Houdini-style
Non-frame files
A sequence with no frame range is valid:
seq = FileSequence('/render/config.json')
seq.frameRange() # ''
seq.frame('') # '/render/config.json'
Pad style — HASH4 (default) vs HASH1 (ie Houdini)
from fileseq import FileSequence, PAD_STYLE_DEFAULT, PAD_STYLE_HASH1
# Default: '#' maps to 4-digit zero-padded
seq = FileSequence('/out/f.1-10#.exr', pad_style=PAD_STYLE_DEFAULT)
# Hash1: '#' maps to 1 digit, '####' to 4 digits (Houdini convention)
seq = FileSequence('/out/f.1-10#.exr', pad_style=PAD_STYLE_HASH1)
Subframe sequences
Subframe support in a sequence pattern must be explicitly enabled, to avoid ambiguous parsing:
seq = FileSequence('/render/beauty.1-2x0.5#.#.exr', allow_subframes=True)
list(seq)
# ['/render/beauty.0001.0000.exr',
# '/render/beauty.0001.5000.exr',
# '/render/beauty.0002.0000.exr']
Formatting
format() accepts a template string with named fields:
seq = FileSequence('/show/shot/beauty.1-100#.exr')
seq.format('{dirname}{basename}{range}{padding}{extension}')
# '/show/shot/beauty.1-100#.exr'
seq.format('{basename}{extension}') # 'beauty.exr'
pathlib variant
FilePathSequence is identical to FileSequence but returns
pathlib.Path objects from iteration and frame():
from fileseq import FilePathSequence
seq = FilePathSequence('/render/beauty.1-10#.exr')
type(seq.frame(1)) # <class 'pathlib.PosixPath'>
Filesystem Discovery
Three class methods locate sequences on disk.
findSequencesOnDisk — scan a directory
from fileseq import FileSequence
# All sequences in a directory
seqs = FileSequence.findSequencesOnDisk('/show/shot/renders/v001')
for s in seqs:
print(s)
# Include hidden files (names starting with '.')
seqs = FileSequence.findSequencesOnDisk('/renders', include_hidden=True)
# Strict padding: only match files whose digit count equals the padding token width
seqs = FileSequence.findSequencesOnDisk('/renders', strictPadding=True)
# Subframe sequences
seqs = FileSequence.findSequencesOnDisk('/renders', allow_subframes=True)
findSequenceOnDisk — find one specific sequence
# Wildcard padding: accepts any number of digits
seq = FileSequence.findSequenceOnDisk('/renders/beauty.@.exr')
# Strict padding: only files that match the token width exactly
seq = FileSequence.findSequenceOnDisk('/renders/beauty.%04d.exr', strictPadding=True)
# Preserve the original padding token in the result
seq = FileSequence.findSequenceOnDisk('/renders/beauty.%02d.exr',
strictPadding=True,
preserve_padding=True)
Using a custom subclass with the find methods
All three find methods are classmethods, so the simplest way to get results of a
custom subclass is to call the method directly on that subclass. All hooks
(_preprocess_sequence, _resolve_padding, getPaddingNum) are picked up
automatically:
# Call directly on the subclass — hooks are used automatically
seqs = VRayFileSequence.findSequencesOnDisk('/renders')
seq = VRayFileSequence.findSequenceOnDisk('/renders/beauty.<frame04>.exr',
strictPadding=True,
preserve_padding=True)
seqs = VRayFileSequence.findSequencesInList(paths)
When the call site cannot be changed — for example in a generic utility that
always calls FileSequence.findSequenceOnDisk — pass the subclass via the
klass argument instead:
# klass overrides which class is used to construct results
seqs = FileSequence.findSequencesOnDisk('/renders', klass=VRayFileSequence)
seq = FileSequence.findSequenceOnDisk('/renders/beauty.<frame04>.exr',
strictPadding=True,
preserve_padding=True,
klass=VRayFileSequence)
seqs = FileSequence.findSequencesInList(paths, klass=VRayFileSequence)
Both approaches produce identical results.
findSequencesInList — build sequences from an existing file list in memory
This is useful when you already have a list of paths (e.g. from an asset database) and do not want a directory scan:
paths = [
'/renders/beauty.0001.exr',
'/renders/beauty.0002.exr',
'/renders/beauty.0003.exr',
'/renders/hero.0001.exr',
]
seqs = FileSequence.findSequencesInList(paths)
# Returns two FileSequence objects: beauty.1-3#.exr and hero.0001#.exr
Normally each path is parsed through the grammar to identify its dirname, basename, extension,
and frame number. When you already know that all paths share the same structure, pass a
using template to skip that per-path parsing. The template’s dirname, basename, and
extension are used to compute character offsets, so the frame number is extracted by a plain
string slice rather than a full parse. This can be significantly faster when the list is large:
# Without template: every path is parsed individually
seqs = FileSequence.findSequencesInList(paths)
# With template: frame extracted via string slicing — no per-path grammar parse
template = FileSequence('/renders/beauty.#.exr')
seqs = FileSequence.findSequencesInList(paths, using=template)
The template is also useful when the filename has an ambiguous structure — for example, a basename that contains digits — and you want to pin exactly which numeric run is the frame number:
paths = [
'/renders/shot_101_beauty.0001.exr',
'/renders/shot_101_beauty.0002.exr',
'/renders/shot_101_beauty.0003.exr',
]
# Without template, the '101' in the name could confuse sequence grouping.
# The template makes the intent unambiguous:
template = FileSequence('/renders/shot_101_beauty.#.exr')
seqs = FileSequence.findSequencesInList(paths, using=template)
# [<FileSequence: '/renders/shot_101_beauty.1-3#.exr'>]
Customizing with Subclasses
Both FileSequence and FilePathSequence inherit from the
abstract base BaseFileSequence. You can subclass either to add
custom behavior by overriding one or both hooks described below.
_preprocess_sequence — translate custom syntax before parsing
Override this method to accept sequence strings that use a non-standard notation. The hook receives the raw input string and must return a string in the grammar that fileseq understands.
VRay uses a <frameNN> token (e.g. <frame04>) to express padding width. This token is
not part of the fileseq grammar, but it maps cleanly to printf-style padding. Using all three
hooks together makes the subclass fully round-trippable — padding() returns the VRay token
and individual frame paths are still correctly zero-padded:
import re
import fileseq
class VRayFileSequence(fileseq.FileSequence):
"""Translate VRay ``<frameNN>`` padding tokens natively."""
_VRAY_PAD_RE = re.compile(r'<frame(\d+)>')
_used_custom_padding = False
def _preprocess_sequence(self, sequence: str) -> str:
"""Translate ``<frameNN>`` → ``%0Nd`` so the grammar accepts it."""
def replace(m):
width = int(m.group(1))
self._used_custom_padding = True
return '%0{}d'.format(width) if width > 0 else '%d'
return self._VRAY_PAD_RE.sub(replace, sequence)
def _resolve_padding(self, parsed_pad: str, zfill: int, pad_style) -> str:
"""Translate the parsed printf form back to the canonical VRay token.
Only do this if preprocessing actually saw and translated a
``<frameNN>`` token. A built-in token that happens to imply the
same width (e.g. ``#``) must not be reinterpreted as VRay's.
"""
if zfill > 0 and self._used_custom_padding:
return '<frame{:02d}>'.format(zfill)
return parsed_pad
@classmethod
def getPaddingNum(cls, chars: str, **kwargs) -> int:
"""Recognise ``<frameNN>`` tokens in addition to the built-in formats."""
m = cls._VRAY_PAD_RE.match(chars)
if m:
return int(m.group(1))
return super().getPaddingNum(chars, **kwargs)
# With a frame range
seq = VRayFileSequence('/render/beauty.1-100<frame04>.exr')
seq.padding() # '<frame04>'
str(seq) # '/render/beauty.1-100<frame04>.exr'
seq.frame(42) # '/render/beauty.0042.exr'
# Pattern-only (no range embedded in the string)
seq = VRayFileSequence('/render/beauty.<frame04>.exr')
seq.padding() # '<frame04>'
seq.frameRange() # ''
# Subframe variant — two tokens, one for frames and one for the sub-second part
seq = VRayFileSequence(
'/render/beauty.1-5<frame04>.10-20<frame04>.exr',
allow_subframes=True,
)
seq.frame(1) # '/render/beauty.0001.0000.exr'
Calling setPadding on a custom-format subclass
Passing a custom token to setPadding (e.g. seq.setPadding('<frame02>')) works correctly
as long as getPaddingNum is overridden to recognize that token — the zfill is updated and
padding() returns the new token.
Passing a built-in token (e.g. seq.setPadding('%04d')) stores it as-is. The setter does
not call _resolve_padding, so the value is not converted to the custom format automatically.
_resolve_padding — canonicalize the internal padding representation
Override this method to store a custom token as the canonical padding form. It is called
immediately after _zfill is computed in _init_impl, before anything is persisted, so
the value it returns becomes what padding(), framePadding(), and str() all see.
The default implementation is a no-op — it returns parsed_pad unchanged, which preserves
existing behavior for all built-in formats.
When to use it: any time padding() should return a custom token rather than the
grammar-normalized form (e.g. %04d). The two hooks are always used together:
_preprocess_sequence translates the input so the grammar accepts it, and
_resolve_padding translates the result back to the custom canonical form before storage.
Override getPaddingNum as well so that setPadding with a custom token is handled
correctly.
See the complete three-override example in the _preprocess_sequence section above.
parsePadding: detect a padding token in an arbitrary string
The hooks above all operate on a fully constructed sequence instance. Sometimes you just need
to answer “does this string contain a padding token at all” for a string that may not be a
complete, valid sequence on its own, such as a single file path with no frame range.
parsePadding is a classmethod for exactly that:
FileSequence.parsePadding('/render/beauty.#.exr') # '#'
FileSequence.parsePadding('/render/beauty.1001.exr') # '' (a resolved frame number, not a token)
FileSequence.parsePadding('/render/beauty.exr') # ''
The default implementation recognizes the built-in fileseq tokens. Override it alongside
_preprocess_sequence to also recognize a custom token, falling back to the built-in behavior
when no custom token is present:
class VRayFileSequence(fileseq.FileSequence):
# ... _preprocess_sequence, _resolve_padding, getPaddingNum as above ...
@classmethod
def parsePadding(cls, sequence: str) -> str:
padding = super().parsePadding(sequence)
if not padding:
m = cls._VRAY_PAD_RE.search(sequence)
if m:
padding = m.group(0)
return padding
VRayFileSequence.parsePadding('/render/beauty.<frame04>.exr') # '<frame04>'
VRayFileSequence.parsePadding('/render/beauty.#.exr') # '#'
VRayFileSequence.parsePadding('/render/beauty.exr') # ''
def has_padding(path: str) -> bool:
return bool(VRayFileSequence.parsePadding(path))
_postprocess_sequence — restore custom syntax on output
This is the complement to _preprocess_sequence. It is called by __str__()
and format() on the fully assembled sequence string just before it
is returned, giving subclasses the opportunity to restore syntax that is not the padding token
itself — for example, a custom frame range delimiter.
import re
import fileseq
class EqualRangeFileSequence(fileseq.FileSequence):
"""Accept ``1=100`` range syntax in addition to the standard ``1-100``."""
_EQUAL_RE = re.compile(r'(\d+)=(\d+)')
def _preprocess_sequence(self, sequence: str) -> str:
return self._EQUAL_RE.sub(r'\1-\2', sequence)
def _postprocess_sequence(self, sequence: str) -> str:
# FrameSet always emits '1-100'; restore the custom delimiter on output.
return sequence.replace('-', '=', 1)
seq = EqualRangeFileSequence('/render/beauty.1=100#.exr')
str(seq) # '/render/beauty.1=100#.exr'
seq.frame(42) # '/render/beauty.0042.exr'
Note
_postprocess_sequence can also be used to restore a custom padding token on output,
but _resolve_padding is the preferred approach for that — it stores the token as the
canonical form so that padding() and str() return it directly.
_postprocess_sequence is best reserved for transforming parts of the string that are
not the padding token (such as the frame range delimiter in the example above).
Scope of _postprocess_sequence
_postprocess_sequence is only called by methods that produce a sequence pattern string
(__str__() and format()).
It is not called when resolving individual frame paths via
frame() or iteration — those use the internally stored padding
directly, so frame paths are always correct regardless of what _postprocess_sequence does.
When the sequence has no frame range, __str__() omits the padding
token entirely. Use format() with an explicit {padding} field
if you need the padding token in the output for a pattern-only sequence.
_create_path — control the type returned for each frame path
Override this method to return a custom path type instead of a plain string. The method receives a fully-resolved path string and must return whatever object your code needs.
from pathlib import Path
from fileseq import FileSequence
class S3FileSequence(FileSequence):
"""Return S3-prefixed paths instead of local paths."""
BUCKET = 's3://my-studio-bucket'
def _create_path(self, path_str: str) -> str:
# Strip the leading slash so the URL is well-formed
return f'{self.BUCKET}/{path_str.lstrip("/")}'
seq = S3FileSequence('/renders/beauty.1-3#.exr')
seq.frame(1) # 's3://my-studio-bucket/renders/beauty.0001.exr'
list(seq)
# ['s3://my-studio-bucket/renders/beauty.0001.exr',
# 's3://my-studio-bucket/renders/beauty.0002.exr',
# 's3://my-studio-bucket/renders/beauty.0003.exr']
Both hooks may be combined in a single subclass.