Skip to content

Differences from ExtendScript

This page documents the intentional design differences between py_aep and the After Effects ExtendScript API. These are not bugs - they are choices made to provide a more Pythonic, convenient, or complete interface.

Naming Conventions

ExtendScript uses camelCase for attributes and methods. py_aep uses snake_case following Python conventions:

ExtendScript py_aep
blendingMode blending_mode
frameRate frame_rate
isTimeVarying is_time_varying

Indexing

ExtendScript collections are 1-based. Python lists and py_aep are 0-based:

var firstLayer = comp.layer(1);
var firstKey = prop.keyValue(1);
first_layer = comp.layers[0]
first_key = prop.keyframes[0].value

Layer.index and PropertyBase.property_index also use 0-based numbering, so that comp.layers[layer.index] and group.properties[prop.property_index] work directly without offset arithmetic.

Collections and Iterators

ExtendScript uses indexed accessor methods (item(index), layer(index)) on custom collection objects (ItemCollection, LayerCollection, OMCollection, RQItemCollection). py_aep uses standard Python lists:

for (var i = 1; i <= comp.numLayers; i++) {
    var layer = comp.layer(i);
}
for layer in comp.layers:
    ...

Keyframes

ExtendScript accesses keyframe data through Property.key*() methods that take a 1-based key index. py_aep exposes keyframes as a list of Keyframe objects on Property.keyframes:

var time = prop.keyTime(1);
var value = prop.keyValue(1);
var inType = prop.keyInInterpolationType(1);
kf = prop.keyframes[0]
kf.time        # seconds
kf.frame_time  # frames
kf.value
kf.in_interpolation_type

The Keyframe object bundles all keyframe attributes together, so you don't need separate method calls for each attribute.

The keyframe mutation methods mirror ExtendScript but use 0-based key indices:

prop.addKey(1.0);
prop.setValueAtTime(1.0, value);
prop.setValuesAtTimes(times, values);
prop.removeKey(1);
prop.add_key(1.0)
prop.set_value_at_time(1.0, value)
prop.set_values_at_times(times, values)
prop.remove_key(0)

These work on every property kind, including the complex ones (mask paths, source text, markers, orientation, gradients). Adding the first keyframe converts a static property to an animated one, and removing the last keyframe reverts it to a static value holding the removed keyframe's value, matching After Effects' on-disk forms. Marker properties accept a plain string as a comment shorthand:

layer["ADBE Marker"].set_value_at_time(2.0, "my comment")

Inserting a keyframe into a segment keeps the segment's curve. After Effects does too, with two exceptions where py_aep deliberately keeps the curve instead (measured on AE 2026):

  • A curved spatial segment. AE gives the new keyframe zero spatial tangents, so the motion path gets a corner there; py_aep splits the path exactly.
  • A color segment with one LINEAR side. AE keeps that side LINEAR and the colors in between shift slightly; py_aep turns it BEZIER, as AE does for every other property, so the colors stay put.

Feather Points

ExtendScript exposes mask feather data as parallel arrays on Shape (featherSegLocs, featherRadii, etc.). py_aep exposes a list of FeatherPoint objects on Shape.feather_points:

var locs = shape.featherSegLocs;
var radii = shape.featherRadii;
var types = shape.featherTypes;
for fp in shape.feather_points:
    fp.seg_loc
    fp.radius
    fp.type

Each FeatherPoint bundles segment location, radius, interpolation, tension, and corner angle together.

Markers

In ExtendScript, CompItem.markerProperty and Layer.marker are Property objects accessed via keyValue(). In py_aep, CompItem.marker_property and Layer.marker expose the underlying Property (with keyframes holding marker times), while CompItem.markers and Layer.markers provide a convenient flat list[MarkerValue]:

var marker = comp.markerProperty.keyValue(1);
marker.comment;
marker = comp.markers[0]
marker.comment
marker.time        # seconds
marker.frame_time  # frames

Frame-Based Time Attributes

ExtendScript expresses all times in seconds (floating-point). py_aep adds integer frame-based equivalents for convenience:

ExtendScript (seconds) py_aep (seconds) py_aep (frames)
Layer.inPoint layer.in_point layer.frame_in_point
Layer.outPoint layer.out_point layer.frame_out_point
Layer.startTime layer.start_time layer.frame_start_time
Item.time item.time item.frame_time
Layer.time layer.time layer.frame_time
- keyframe.time keyframe.frame_time
MarkerValue.duration marker.duration marker.frame_duration

Warning: AVItem.frame_duration is the total duration in frames (an integer), not the duration of a single frame in seconds (which is 1/frame_rate). This differs from ExtendScript's AVItem.frameDuration which is the duration of one frame in seconds.

Layer In and Out Points

Writing layer.in_point moves only the in point. In After Effects 2026, writing inPoint also moves the out point: the stored out point shifts by the new in point (composition seconds) minus the old one (layer seconds), held inside the source's duration. On a 100 % layer that starts at 0 this keeps the layer's duration; with any other start or stretch the out point lands somewhere else (a 150 % solid starting at 0 with its out point at 45 s ends at 48 s after inPoint = 2). Set out_point after in_point to place both ends.

Convenience Access Properties

Project

Project provides filtered views of items that ExtendScript requires manual filtering for:

project.compositions   # list[CompItem] - all compositions
project.folders        # list[FolderItem] - all folders
project.footages       # list[FootageItem] - all footages

FolderItem

FolderItem provides filtered item lists:

folder.compositions    # list[CompItem] - compositions in the folder
folder.folders         # list[FolderItem] - subfolders
folder.footages        # list[FootageItem] - footages in the folder

CompItem

CompItem provides filtered layer lists:

comp.text_layers             # list[TextLayer]
comp.shape_layers            # list[ShapeLayer]
comp.camera_layers           # list[CameraLayer]
comp.light_layers            # list[LightLayer]
comp.parametric_mesh_layers  # list[ParametricMeshLayer]
comp.null_layers             # list[Layer]
comp.solid_layers            # list[AVLayer]
comp.adjustment_layers       # list[AVLayer]
comp.three_d_layers          # list[AVLayer]
comp.guide_layers            # list[AVLayer]
comp.solo_layers             # list[Layer]
comp.composition_layers      # list[AVLayer] - layers sourced from comps
comp.footage_layers          # list[AVLayer] - layers sourced from footages
comp.file_layers             # list[AVLayer] - layers sourced from files
comp.placeholder_layers      # list[AVLayer]
comp.av_layers               # list[AVLayer] - all AV layers

Extra Attributes

py_aep exposes additional attributes parsed from the binary format that are not available in ExtendScript:

Property

Attribute Description
dimensions Number of dimensions (1, 2, or 3)
locked_ratio True if X/Y ratio is locked (a Scale or Mask Feather's constrain-proportions switch)
default_value The default value of the property
last_value The last value before animation
nb_options Number of options in a dropdown property

FootageItem

Attribute Description
asset_type The footage type ("placeholder", "solid", "file")
start_frame The footage start frame
end_frame The footage end frame

CompItem

Attribute Description
time_scale Internal time scale divisor for keyframe times
essential_graphics_controllers List of Essential Graphics controllers in the comp
guides List of Guide objects (ruler guides for alignment)
renderer_options The active 3D renderer's options

Layer

Attribute Description
layer_type The layer type ("AVLayer", "Layer", "CameraLayer", "LightLayer" "ParametricMeshLayer")

RenderQueueItem

Attribute Description
comment A comment describing the render queue item
name The render settings template name
settings Full render settings as a dict

OutputModule

Attribute Description
file_template Raw file path template with variables like [compName]
format_options Format-specific options (JPEG quality, EXR compression, etc.)
settings Full output module settings as a dict

Enums

py_aep provides many enum classes across 8 modules, covering values that ExtendScript exposes as plain integers or doesn't expose at all:

  • Format options enums: VideoCodec, AudioCodec, OpenExrCompression, etc.
  • Render settings enums: FieldRender, MotionBlurSetting, DiskCacheSetting, etc.
  • Output module enums: OutputChannels, OutputColorDepth, ResizeQuality, etc.
  • Font enums: CTFontTechnology, CTFontType, CTScript
  • Text enums: ComposerEngine, BoxAutoFitPolicy, LineOrientation, etc.

Gradient Colors

ExtendScript reports gradient color properties (ADBE Vector Grad Colors) with propertyValueType = NO_VALUE and provides no .value accessor. py_aep parses the underlying XML stored in the binary and exposes it as a Gradient object on Property.value:

gfill = contents.property("ADBE Vector Graphic - G-Fill")
colors = gfill.property("ADBE Vector Grad Colors")
gradient = colors.value  # Gradient instance

for stop in gradient.color_stops:
    print(stop.offset, stop.color)  # (red, green, blue) tuple

for stop in gradient.alpha_stops:
    print(stop.offset, stop.alpha)

Output Module Format Options

ExtendScript provides no access to format-specific render settings. py_aep parses these from the binary and exposes them:

  • CineonFormatOptions - black/white points, gamma, bit depth
  • JpegFormatOptions - quality, format type, scans
  • OpenExrFormatOptions - compression, luminance/chroma, bit depth
  • PngFormatOptions - bit depth, compression, HDR10 metadata
  • TargaFormatOptions - bits per pixel, RLE compression
  • TiffFormatOptions - LZW compression, byte order
  • XmlFormatOptions - video/audio codec, frame rate, MPEG settings

3D Renderer Options

CompItem.renderer names the active 3D renderer, but ExtendScript exposes nothing about that renderer's own settings — the Options dialog beside the 3D Renderer dropdown in Composition Settings. py_aep parses them from the binary and exposes them on CompItem.renderer_options:

comp.renderer_options["Quality"] = 61     # keyed by dialog label
comp.renderer_options.quality = 61        # or as a typed attribute

The options available depend on the active renderer:

  • ClassicRendererOptions — shadow map resolution
  • AdvancedRendererOptions — quality, environment light shadow resolution and smoothness, casting box size and centre
  • Cinema4DRendererOptions — quality
  • RayTracedRendererOptions — none; see Known Limitations

Every option py_aep exposes is stored in the .aep and survives an After Effects preferences reset.

Advanced 3D's casting box values are stored as fractions of the composition's raw pixel dimensions and are exposed here as the pixel values the dialog shows. Pixel aspect ratio is not applied, and resizing a composition rescales these options in pixel terms because the stored fraction does not move — both are After Effects' own behaviour.

Stricter Write Validation

py_aep validates values like After Effects' dialogs, which is sometimes stricter than ExtendScript's own setters. Where AE scripting accepts a degenerate value and silently misbehaves, py_aep raises instead:

  • Render time spans: AE scripting accepts a timeSpanStart before 0 or past the span end, then silently renders garbage (a span starting at -5 renders 5 seconds of void lead-in; an end before the start renders a single frame, both with a DONE status - probed in AE 2026). py_aep rejects a negative start, a start at or past the end, and a duration below one frame. The semantics match ExtendScript: setting the duration keeps the start; setting the start keeps the span end fixed while the span still follows the work area or the composition (the duration is recomputed), and keeps the duration once the span is custom.
  • Booleans: AE coerces any truthy value, so "no" becomes True. py_aep boolean attributes and settings accept only True / False.

Approximated Runtime Behaviors

Some ExtendScript methods gate their behavior on runtime state that only a running After Effects has. py_aep implements the closest file-level equivalent and documents the divergence:

  • Project.auto_fix_expressions(): After Effects only rewrites expressions that are currently erroring - runtime state that is not stored in the project file (verified: broken and working expressions are chunk-identical on disk). py_aep instead replaces the quoted forms "old_text" / 'old_text' in every enabled expression. Everything else matches AE (probed in AE 2026): both quote styles are fixed, disabled expressions are never touched, unquoted mentions are ignored, and quoted occurrences inside comments of a rewritten expression are replaced too. The one divergent case: an expression that evaluates cleanly but contains the quoted text is rewritten by py_aep, while AE leaves it alone.
  • Project.revision: py_aep reports the counter stored in the file. After Effects also counts the edits it makes itself while opening the project, so its revision reads that many higher (13, 64, 151 and 312 more on four probed files, AE 2026).
  • AVLayer geometry methods (source_point_to_comp(), comp_point_to_source(), source_rect_at_time()): After Effects evaluates these at the current playhead position, which py_aep reads as the comp's stored time attribute - deterministic per file, but it reflects wherever the playhead sat when the project was last saved. The two point conversions accept an optional time keyword (py_aep extension) to evaluate at an explicit time instead. Auto-orientation along a path and towards the camera is modelled, as are camera and light parents; the point conversions raise NotImplementedError for the cases not measured - separated Position dimensions along a path, a parented layer turned towards the camera, and characters turned towards the camera. All three read transform and content values before expressions (see Limitations). Like After Effects, source_rect_at_time() takes its time in layer time - not offset by the layer's start time nor stretched - where the point conversions take comp time. It raises NotImplementedError for a 3D model layer, whose box After Effects takes from the model geometry, and for a shape layer using path operations other than Trim Paths and Repeater or a polystar with a fractional number of points. For a text layer it measures the glyph outlines of the fonts installed on this machine (a different font version than the one the project was saved with can change the box), and raises for text animators, text on a path, per-character 3D, faux bold / italic, small or all caps, horizontal / vertical scale, non-left-justified box text, and the features the box-text composer refuses; it needs uharfbuzz, which Python 3.7 lacks. TrueType curve extremes whose off-curve point overshoots the curve can differ from AE's by a fraction of a font unit (0.03 px on Calibri's W at 100 pt). calculate_transform_from_points() names its third parameter point_bottom_left: the AE guide calls it pointBottomRight, but After Effects treats it as the bottom-left corner (probed AE 2026; the guide's own example passes bl).
  • TextDocument.reset_char_style() / reset_paragraph_style(): After Effects restores the Character / Paragraph panel defaults, which are application state rather than project data - they live in the AE preferences (["Text Style Sheet"] / ["Text Paragraph Sheet"] in Prefs-text.txt), not in the .aep. py_aep reads those same sections from the preferences directory the project was parsed with (see parse(..., ae_preferences_dir=...)), so a reset restores your panel defaults exactly as AE would. Parsed without a preferences directory, it falls back to AE's factory values. Only the attributes present in those panel sheets are restored - which is why, like After Effects, a paragraph reset leaves auto_hyphenate untouched.
  • TextDocument.baseline_locs: reports the layout After Effects persisted. The per-line pen origins and glyph advances exist only in that cache, so - unlike composed_line_count - this never recomposes; after a layout-affecting py-side write the values stay at the persisted layout (see composition_stale).
  • Project.replace_font(): no_font_locking is accepted for parity and ignored. After Effects uses it to suppress the fallback font it picks when the target font lacks glyphs for the text; that is a runtime font-engine decision py_aep does not make, so py_aep always performs the direct replacement (it behaves as if no_font_locking were True). Like every py_aep text write, the layer's layout cache is left as-is (AE recomputes it on open), so glyph advances cached for the old font stay until then.