How do I programmatically set hair position and shape in Blender 2.8?
Halfway there now…

Following this answer for manipulating particles, and shoehorning this problem with the same approach led to the following:




Viewport: OK! :D
Render preview: OK! :D
Final render: Not OK... :(


Code:

import bpy

# 1. Use initial cube as object and set CYCLES
bpy.context.scene.render.engine = 'CYCLES'
obj = bpy.data.objects["Cube"]

# 2. Add a particle system
psys = obj.modifiers.new("hair", 'PARTICLE_SYSTEM').particle_system
psys.settings.type = 'HAIR'
obj.show_instancer_for_render = False
obj.show_instancer_for_viewport = False
psys.settings.use_strand_primitive = True
psys.settings.radius_scale = 0.2
psys.settings.hair_step = 3
psys.settings.count = 5

# 2½. Animation options
psys.settings.frame_start = 1
psys.settings.frame_end = 1
psys.settings.lifetime = 1000

# 3. Setup frame_change_post-callback
depsgraph = bpy.context.evaluated_depsgraph_get()           # Get depsgraph

# Define particle setter. Set at every frame update(?)
def particleSetter(self):
    particle_systems = obj.evaluated_get(depsgraph).particle_systems

    for m, particle in enumerate(particle_systems[0].particles):
        setattr(particle, "location", (m, 0, 0))
        for n in range(len(particle.hair_keys)):
            setattr(particle.hair_keys[n], "co", (m, 0, n))

    # Toggle back and forth to update viewport
    bpy.ops.particle.particle_edit_toggle()
    bpy.ops.particle.particle_edit_toggle()

# Clear the post frame handler
bpy.app.handlers.frame_change_post.clear()

# Register our particleSetter with the post frame handler
bpy.app.handlers.frame_change_post.append(particleSetter)

# Trigger frame update
bpy.context.scene.frame_current = 0