2025, Nov 11 15:00
Avoid AttributeError: 'int' object has no attribute 'duration' in MoviePy by unpacking dict values for concatenate_videoclips
Learn why unpacking a dict into a clip list causes MoviePy's concatenate_videoclips to raise AttributeError, and fix it with dict.values() or a list.
Fixing a silent Python pitfall in MoviePy: when unpacking a dictionary into a list intended for concatenate_videoclips, you may end up injecting integers instead of clips. The result is an AttributeError at runtime that points into MoviePy, while the real cause sits in how the sequence is built.
Reproducing the issue
The following snippet composes a title card, loads images from a directory, applies simple fades, and attempts to concatenate everything into a single video. The bug appears when a dictionary is unpacked into the clip list.
from moviepy.editor import *
from moviepy.config import change_settings
from pathlib import Path
import os
import traceback
change_settings({"IMAGEMAGICK_BINARY": r"C:\\Program Files\\ImageMagick-7.1.2-Q16-HDRI\\magick.exe"})
def looks_like_image(fp):
"""
Checks if a file is likely an image based on its extension.
"""
valid_ext = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp'}
_, ext = os.path.splitext(fp)
return ext.lower() in valid_ext
clip_map = {}
user_tag = "user_"
assets_dir = Path(r"C:/Users/User/Development/moviepy/images")
idx = 0
for p in assets_dir.iterdir():
if p.is_file():
if os.path.isfile(p) and looks_like_image(p):
clip_map[idx] = ImageClip(str(p)).set_duration(2)
clip_map[idx] = clip_map[idx].fx(vfx.fadeout, 1)
clip_map[idx] = clip_map[idx].fx(vfx.fadein, 1)
idx += 1
else:
print(f"'{p}' is not an image file (or doesn't exist).")
audio_track = AudioFileClip(r'C:\Users\User\Development\moviepy\audio\KYJellyBabies-AccessDenied.mp3')
title_clip = TextClip(
"In Loving memory of \nJoe Blocks",
fontsize=70,
color='white',
font='Arial',
bg_color='transparent'
).set_pos('center').set_duration(5)
combined_clips = [
title_clip,
*clip_map, # <-- problem: expands dictionary KEYS
]
try:
final_cut = concatenate_videoclips(combined_clips, method='compose')
final_cut.audio = audio_track.subclip(0, final_cut.duration)
final_cut.write_videofile('final_video.mp4', fps=30)
except Exception as err:
traceback.print_exc()
At runtime this surfaces as:
AttributeError: 'int' object has no attribute 'duration'
What’s actually going on
The unpacking operator * works differently for different container types. When you write *dict_obj in Python, you don’t get its values; you get its keys. In this case, keys are integers added during enumeration. Those integers are unpacked into the list of clips and then passed to MoviePy. concatenate_videoclips expects each element to be a clip-like object with a duration attribute. An integer doesn’t have duration, hence the AttributeError.
Two straightforward fixes
If you truly want to keep building a dictionary, unpack its values instead of its keys. That’s a minimal change that preserves the rest of the flow.
combined_clips = [
title_clip,
*clip_map.values(), # expands ImageClip objects, not dict keys
]
Alternatively, build a list from the start. This keeps the semantics aligned with how the result is consumed and avoids accidental key expansion.
from moviepy.editor import *
from moviepy.config import change_settings
from pathlib import Path
import os
import traceback
change_settings({"IMAGEMAGICK_BINARY": r"C:\\Program Files\\ImageMagick-7.1.2-Q16-HDRI\\magick.exe"})
def looks_like_image(fp):
valid_ext = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp'}
_, ext = os.path.splitext(fp)
return ext.lower() in valid_ext
clip_list = [] # use a list, then append clips as you go
user_tag = "user_"
assets_dir = Path(r"C:/Users/User/Development/moviepy/images")
for p in assets_dir.iterdir():
if p.is_file():
if os.path.isfile(p) and looks_like_image(p):
clip_list.append(
ImageClip(str(p))
.set_duration(2)
.fx(vfx.fadeout, 1)
.fx(vfx.fadein, 1)
)
else:
print(f"'{p}' is not an image file (or doesn't exist).")
audio_track = AudioFileClip(r'C:\Users\User\Development\moviepy\audio\KYJellyBabies-AccessDenied.mp3')
title_clip = TextClip(
"In Loving memory of \nJoe Blocks",
fontsize=70,
color='white',
font='Arial',
bg_color='transparent'
).set_pos('center').set_duration(5)
combined_clips = [
title_clip,
*clip_list,
]
try:
final_cut = concatenate_videoclips(combined_clips, method='compose')
final_cut.audio = audio_track.subclip(0, final_cut.duration)
final_cut.write_videofile('final_video.mp4', fps=30)
except Exception as err:
traceback.print_exc()
Why this detail matters
APIs like MoviePy assume the sequences you pass in contain media objects with specific attributes and methods. Accidentally mixing in scalars because of * expansion on a dictionary breaks those assumptions and defers the failure to library code, making the error look unrelated. Understanding how Python unpacks iterables prevents these hard-to-spot type mismatches, especially in pipelines where elements are composed and transformed across several steps.
Takeaways
Before concatenating, be explicit about the sequence you’re building. If you rely on a dictionary, unpack .values() where objects are required. If you don’t need key-based access, prefer a list and append directly as you generate ImageClip instances. If something goes wrong, a complete traceback helps pinpoint where a non-clip slipped into the pipeline. Keeping an eye on what exactly gets unpacked will save time the next time you wire MoviePy clips together.